0
# Balance Change Testing
1
2
Matchers for testing ETH and ERC-20 token balance changes during transaction execution, supporting both single and multiple account scenarios.
3
4
## Capabilities
5
6
### Ether Balance Change Testing
7
8
Tests if a transaction changes the ETH balance of a specific account by an expected amount.
9
10
```typescript { .api }
11
/**
12
* Tests if transaction changes ETH balance of an account
13
* @param account - Account whose balance change to test (Addressable or string)
14
* @param balance - Expected balance change as BigNumberish or predicate function
15
* @param options - Optional configuration for gas calculations
16
* @returns AsyncAssertion promise that resolves if balance change matches
17
*/
18
changeEtherBalance(
19
account: any,
20
balance: any,
21
options?: any
22
): AsyncAssertion;
23
```
24
25
**Usage Examples:**
26
27
```typescript
28
import { expect } from "chai";
29
import { ethers } from "hardhat";
30
31
// Test ETH balance increase
32
await expect(contract.withdraw(ethers.parseEther("1")))
33
.to.changeEtherBalance(owner, ethers.parseEther("1"));
34
35
// Test ETH balance decrease
36
await expect(contract.deposit({ value: ethers.parseEther("0.5") }))
37
.to.changeEtherBalance(owner, ethers.parseEther("-0.5"));
38
39
// Test no balance change
40
await expect(contract.viewFunction())
41
.to.changeEtherBalance(owner, 0);
42
43
// Test with gas fee inclusion options
44
await expect(contract.transaction())
45
.to.changeEtherBalance(owner, expectedChange, { includeFee: true });
46
47
// Test with custom predicate function
48
await expect(contract.variableWithdraw())
49
.to.changeEtherBalance(owner, (change: bigint) => change > ethers.parseEther("0.1"));
50
```
51
52
### Multiple Ether Balance Changes
53
54
Tests ETH balance changes for multiple accounts simultaneously in a single transaction.
55
56
```typescript { .api }
57
/**
58
* Tests ETH balance changes for multiple accounts
59
* @param accounts - Array of accounts to test (Addressable[] or string[])
60
* @param balances - Expected balance changes array or validation function
61
* @param options - Optional configuration for gas calculations
62
* @returns AsyncAssertion promise that resolves if all balance changes match
63
*/
64
changeEtherBalances(
65
accounts: any[],
66
balances: any[] | ((changes: bigint[]) => boolean),
67
options?: any
68
): AsyncAssertion;
69
```
70
71
**Usage Examples:**
72
73
```typescript
74
// Test multiple specific balance changes
75
await expect(contract.distribute([user1, user2], [100, 200]))
76
.to.changeEtherBalances(
77
[owner, user1, user2],
78
[ethers.parseEther("-0.3"), ethers.parseEther("0.1"), ethers.parseEther("0.2")]
79
);
80
81
// Test with predicate function for complex validation
82
await expect(contract.complexDistribution())
83
.to.changeEtherBalances(
84
[owner, user1, user2],
85
(changes: bigint[]) => {
86
return changes[0] < 0n && // owner balance decreases
87
changes[1] > 0n && // user1 balance increases
88
changes[2] > 0n && // user2 balance increases
89
changes[0] + changes[1] + changes[2] === 0n; // total is conserved
90
}
91
);
92
93
// Test with gas fee considerations
94
await expect(contract.batchTransfer(recipients, amounts))
95
.to.changeEtherBalances(
96
[sender, ...recipients],
97
expectedChanges,
98
{ includeFee: true }
99
);
100
```
101
102
### Token Balance Change Testing
103
104
Tests if a transaction changes the ERC-20 token balance of a specific account.
105
106
```typescript { .api }
107
/**
108
* Tests if transaction changes token balance of an account
109
* @param token - ERC-20 token contract with balanceOf method
110
* @param account - Account whose token balance to test (Addressable or string)
111
* @param balance - Expected balance change as BigNumberish or predicate function
112
* @returns AsyncAssertion promise that resolves if token balance change matches
113
*/
114
changeTokenBalance(
115
token: any,
116
account: any,
117
balance: any
118
): AsyncAssertion;
119
```
120
121
**Usage Examples:**
122
123
```typescript
124
// Test token balance increase
125
await expect(token.mint(user, 1000))
126
.to.changeTokenBalance(token, user, 1000);
127
128
// Test token balance decrease
129
await expect(token.burn(500))
130
.to.changeTokenBalance(token, owner, -500);
131
132
// Test token transfer between accounts
133
const transferTx = token.transfer(recipient, 250);
134
await expect(transferTx).to.changeTokenBalance(token, owner, -250);
135
await expect(transferTx).to.changeTokenBalance(token, recipient, 250);
136
137
// Test with predicate function
138
await expect(token.variableMint(user))
139
.to.changeTokenBalance(token, user, (change: bigint) => change > 100n && change <= 1000n);
140
141
// Test no balance change for view functions
142
await expect(token.totalSupply())
143
.to.changeTokenBalance(token, user, 0);
144
```
145
146
### Multiple Token Balance Changes
147
148
Tests token balance changes for multiple accounts simultaneously.
149
150
```typescript { .api }
151
/**
152
* Tests token balance changes for multiple accounts
153
* @param token - ERC-20 token contract with balanceOf method
154
* @param accounts - Array of accounts to test (Addressable[] or string[])
155
* @param balances - Expected balance changes array or validation function
156
* @returns AsyncAssertion promise that resolves if all token balance changes match
157
*/
158
changeTokenBalances(
159
token: any,
160
accounts: any[],
161
balances: any[] | ((changes: bigint[]) => boolean)
162
): AsyncAssertion;
163
```
164
165
**Usage Examples:**
166
167
```typescript
168
// Test multiple token balance changes
169
await expect(token.batchTransfer([user1, user2], [100, 200]))
170
.to.changeTokenBalances(
171
token,
172
[owner, user1, user2],
173
[-300, 100, 200]
174
);
175
176
// Test with validation function
177
await expect(token.distribute(users, amounts))
178
.to.changeTokenBalances(
179
token,
180
[owner, ...users],
181
(changes: bigint[]) => {
182
const ownerChange = changes[0];
183
const userChanges = changes.slice(1);
184
const totalDistributed = userChanges.reduce((sum, change) => sum + change, 0n);
185
return ownerChange === -totalDistributed && userChanges.every(change => change > 0n);
186
}
187
);
188
189
// Test complex multi-token scenarios
190
await expect(multiTokenContract.swapTokens(tokenA, tokenB, 1000))
191
.to.changeTokenBalances(tokenA, [owner, pool], [-1000, 1000])
192
.and.changeTokenBalances(tokenB, [owner, pool], [950, -950]); // With fees
193
```
194
195
## Advanced Balance Testing Patterns
196
197
### Gas Fee Handling
198
199
```typescript
200
// Include gas fees in ETH balance calculations
201
await expect(contract.payableFunction({ value: ethers.parseEther("1") }))
202
.to.changeEtherBalance(owner, ethers.parseEther("-1"), { includeFee: true });
203
204
// Exclude gas fees (default behavior)
205
await expect(contract.payableFunction({ value: ethers.parseEther("1") }))
206
.to.changeEtherBalance(contract, ethers.parseEther("1")); // Contract receives full amount
207
```
208
209
### Complex Balance Validations
210
211
```typescript
212
// Validate balance conservation in swaps
213
await expect(dex.swap(tokenA, tokenB, 1000))
214
.to.changeTokenBalances(
215
tokenA,
216
[trader, pool],
217
(changes: bigint[]) => changes[0] + changes[1] === 0n // Conservation
218
);
219
220
// Test fee distribution
221
await expect(protocol.collectFees())
222
.to.changeEtherBalances(
223
[treasury, devFund, stakingPool],
224
(changes: bigint[]) => {
225
const [treasuryFee, devFee, stakingFee] = changes;
226
return treasuryFee > 0n &&
227
devFee > 0n &&
228
stakingFee > 0n &&
229
treasuryFee === devFee * 2n; // Treasury gets 2x dev fund
230
}
231
);
232
```
233
234
### Error Handling
235
236
```typescript
237
// Test insufficient balance scenarios
238
await expect(token.transfer(recipient, 1000000))
239
.to.be.revertedWith("Insufficient balance");
240
241
await expect(token.transfer(recipient, 1000000))
242
.to.not.changeTokenBalance(token, owner, -1000000); // No change due to revert
243
```
244
245
## Important Limitations
246
247
**Chaining Restrictions**: Balance matchers do not support chaining with other async matchers:
248
249
```typescript
250
// ❌ This won't work
251
await expect(contract.transfer(recipient, 1000))
252
.to.changeTokenBalance(token, recipient, 1000)
253
.and.to.emit(contract, "Transfer");
254
255
// ✅ Use separate assertions instead
256
const tx = contract.transfer(recipient, 1000);
257
await expect(tx).to.changeTokenBalance(token, recipient, 1000);
258
await expect(tx).to.emit(contract, "Transfer");
259
```
260
261
## Token Interface Requirements
262
263
For token balance testing, the token contract must implement:
264
265
```typescript { .api }
266
interface Token {
267
balanceOf(account: string): Promise<bigint>;
268
// Standard ERC-20 methods for context
269
name(): Promise<string>;
270
symbol(): Promise<string>;
271
transfer(to: string, amount: BigNumberish): Promise<TransactionResponse>;
272
}
273
```
274
275
## Types
276
277
```typescript { .api }
278
interface AsyncAssertion extends Assertion, Promise<void> {}
279
```