or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

argument-utilities.mdbalance-matchers.mdbignumber-support.mdevent-matchers.mdindex.mdpanic-codes.mdrevert-matchers.mdvalidation-matchers.md

balance-matchers.mddocs/

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

```