Skip to content

Commit cc6d57b

Browse files
authored
feat(sdk): support fractional basis points via number type (#8378)
1 parent 02c499d commit cc6d57b

20 files changed

Lines changed: 342 additions & 69 deletions

.changeset/brave-bears-smile.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
"@hyperlane-xyz/sdk": major
3+
---
4+
5+
The bps type was changed from bigint to number throughout the LinearFee fee system to support fractional basis points (e.g., 1.5 bps).
6+
7+
Breaking changes:
8+
- `convertToBps()` return type changed from `bigint` to `number`
9+
- `convertFromBps()` parameter type changed from `bigint` to `number`
10+
- `LinearFeeConfig.bps` and `LinearFeeInputConfig.bps` types changed from `bigint` to `number`
11+
- `ZBps` schema no longer accepts `bigint` input — callers using `bps: 5n` must change to `bps: 5`
12+
- `TokenFeeConfigSchema` and `LinearFeeConfigSchema` bps field type changed from `bigint` to `number`

typescript/cli/src/tests/ethereum/warp/apply/warp-apply-ownership-updates.e2e-test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ describe('hyperlane warp apply E2E (ownership updates)', async function () {
187187
const tokenFeeConfig = {
188188
type: TokenFeeType.LinearFee as const,
189189
owner: feeOwner,
190-
bps: BigInt('100'),
190+
bps: 100,
191191
};
192192

193193
const warpDeployConfig = fixture.getDeployConfig();

typescript/cli/src/tests/ethereum/warp/warp-deploy-2.e2e-test.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -278,7 +278,7 @@ describe('hyperlane warp deploy e2e tests', async function () {
278278
const tokenFee = {
279279
type: TokenFeeType.LinearFee,
280280
token: tokenChain2.address,
281-
bps: 1n,
281+
bps: 1,
282282
};
283283

284284
const warpConfig = WarpRouteDeployConfigSchema.parse({
@@ -316,7 +316,7 @@ describe('hyperlane warp deploy e2e tests', async function () {
316316
const tokenFee = {
317317
type: TokenFeeType.LinearFee,
318318
owner: ownerAddress,
319-
bps: 1n,
319+
bps: 1,
320320
};
321321

322322
const warpConfig = WarpRouteDeployConfigSchema.parse({
@@ -419,7 +419,7 @@ describe('hyperlane warp deploy e2e tests', async function () {
419419
owner: ownerAddress,
420420
tokenFee: {
421421
type: TokenFeeType.LinearFee,
422-
bps: 100n,
422+
bps: 100,
423423
},
424424
},
425425
});
@@ -622,7 +622,7 @@ describe('hyperlane warp deploy e2e tests', async function () {
622622
});
623623

624624
it('should deploy a synthetic token with LinearFee using only bps (computed path)', async () => {
625-
const providedBps = 100n;
625+
const providedBps = 100;
626626

627627
const warpConfig = WarpRouteDeployConfigSchema.parse({
628628
[CHAIN_NAME_2]: {
@@ -661,7 +661,7 @@ describe('hyperlane warp deploy e2e tests', async function () {
661661
expect(syntheticConfig.tokenFee?.type).to.equal(TokenFeeType.LinearFee);
662662

663663
const tokenFee = syntheticConfig.tokenFee as LinearFeeConfig;
664-
expect(BigInt(tokenFee.bps)).to.equal(providedBps);
664+
expect(tokenFee.bps).to.equal(providedBps);
665665
expect(tokenFee.maxFee).to.exist;
666666
expect(tokenFee.halfAmount).to.exist;
667667
expect(BigInt(tokenFee.maxFee) > 0n).to.be.true;

typescript/infra/config/environments/mainnet3/warp/configGetters/getEclipseUSDCWarpConfig.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,7 @@ export const buildEclipseUSDCWarpConfig = async (
240240
tokenFee: getFixedRoutingFeeConfig(
241241
getWarpFeeOwner(chain),
242242
destinations,
243-
5n,
243+
5,
244244
feeParams,
245245
),
246246
};

typescript/infra/config/environments/mainnet3/warp/configGetters/getEniWarpConfigs.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ const owners = {
2525
polygon: '0x3211A1Fea94cd4000Bd82D7C9E9334E51938De1b',
2626
} as const;
2727

28-
const WARP_FEE_BPS = 8n;
28+
const WARP_FEE_BPS = 8;
2929

3030
const usdcTokenAddresses = {
3131
arbitrum: tokens.arbitrum.USDC,

typescript/infra/config/environments/mainnet3/warp/configGetters/getVictionETHWarpConfig.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ export const getVictionETHWarpConfig = async (
7878
tokenFee: getFixedRoutingFeeConfig(
7979
getWarpFeeOwner(currentChain),
8080
feeDestinations,
81-
10n,
81+
10,
8282
),
8383
proxyAdmin: {
8484
owner:

typescript/infra/config/environments/mainnet3/warp/configGetters/utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ export const getNativeTokenConfigForChain = <
195195
export function getFixedRoutingFeeConfig(
196196
owner: Address,
197197
feeDestinations: readonly ChainName[],
198-
bps: bigint,
198+
bps: number,
199199
feeParams?: Record<string, { maxFee: string; halfAmount: string }>,
200200
): TokenFeeConfigInput {
201201
const feeContracts: Record<ChainName, TokenFeeConfigInput> = {};

typescript/sdk/src/fee/EvmTokenFeeDeployer.hardhat-test.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { MultiProvider } from '../providers/MultiProvider.js';
1010

1111
import { EvmTokenFeeDeployer } from './EvmTokenFeeDeployer.js';
1212
import { BPS, HALF_AMOUNT, MAX_FEE } from './EvmTokenFeeReader.hardhat-test.js';
13+
import { BPS_PRECISION } from './utils.js';
1314
import {
1415
LinearFeeConfig,
1516
ProgressiveFeeConfig,
@@ -145,7 +146,9 @@ describe('EvmTokenFeeDeployer', () => {
145146
](1, addressToBytes32(signer.address), amount);
146147

147148
expect(quote.length).to.equal(1);
148-
expect(quote[0].amount).to.be.equal((BigInt(amount) * BPS) / 10_000n);
149+
expect(quote[0].amount).to.be.equal(
150+
(BigInt(amount) * BigInt(BPS)) / BPS_PRECISION,
151+
);
149152
expect(quote[0].token).to.equal(token.address);
150153

151154
// If no fee contract is set, the quote should be zero

typescript/sdk/src/fee/EvmTokenFeeModule.hardhat-test.ts

Lines changed: 57 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@ import { normalizeConfig } from '../utils/ism.js';
1313

1414
import { EvmTokenFeeModule } from './EvmTokenFeeModule.js';
1515
import { BPS, HALF_AMOUNT, MAX_FEE } from './EvmTokenFeeReader.hardhat-test.js';
16-
import { TokenFeeReaderParams } from './EvmTokenFeeReader.js';
16+
import {
17+
EvmTokenFeeReader,
18+
TokenFeeReaderParams,
19+
} from './EvmTokenFeeReader.js';
1720
import {
1821
LinearFeeConfig,
1922
ResolvedTokenFeeConfigInput,
@@ -70,7 +73,11 @@ describe('EvmTokenFeeModule', () => {
7073
});
7174
const onchainConfig = await module.read();
7275
expect(normalizeConfig(onchainConfig)).to.deep.equal(
73-
normalizeConfig({ ...config, maxFee: MAX_FEE, halfAmount: HALF_AMOUNT }),
76+
normalizeConfig({
77+
...config,
78+
maxFee: MAX_FEE,
79+
halfAmount: HALF_AMOUNT,
80+
}),
7481
);
7582
});
7683

@@ -172,7 +179,7 @@ describe('EvmTokenFeeModule', () => {
172179
chain: test4Chain,
173180
config,
174181
});
175-
const updatedConfig = { ...config, bps: BPS + 1n };
182+
const updatedConfig = { ...config, bps: BPS + 1 };
176183
await expectTxsAndUpdate(module, updatedConfig, 0);
177184
const onchainConfig = await module.read();
178185
assert(
@@ -202,7 +209,7 @@ describe('EvmTokenFeeModule', () => {
202209
feeContracts: {
203210
[test4Chain]: {
204211
...feeContracts[test4Chain],
205-
bps: BPS + 1n,
212+
bps: BPS + 1,
206213
},
207214
},
208215
};
@@ -254,7 +261,10 @@ describe('EvmTokenFeeModule', () => {
254261
...routingFeeConfig,
255262
owner: newOwner,
256263
feeContracts: {
257-
[test4Chain]: { ...feeContracts[test4Chain], owner: newOwner },
264+
[test4Chain]: {
265+
...feeContracts[test4Chain],
266+
owner: newOwner,
267+
},
258268
},
259269
},
260270
2,
@@ -297,7 +307,7 @@ describe('EvmTokenFeeModule', () => {
297307
feeContracts: {
298308
[test4Chain]: {
299309
...feeContracts[test4Chain],
300-
bps: BPS + 1n,
310+
bps: BPS + 1,
301311
},
302312
},
303313
};
@@ -373,7 +383,7 @@ describe('EvmTokenFeeModule', () => {
373383
type: TokenFeeType.LinearFee,
374384
owner: signer.address,
375385
token: zeroSupplyToken.address,
376-
bps: 8n,
386+
bps: 8,
377387
};
378388

379389
const expandedConfig = await EvmTokenFeeModule.expandConfig({
@@ -386,16 +396,16 @@ describe('EvmTokenFeeModule', () => {
386396
expandedConfig.type === TokenFeeType.LinearFee,
387397
`Must be ${TokenFeeType.LinearFee}`,
388398
);
389-
const linearConfig = expandedConfig as LinearFeeConfig;
390-
expect(linearConfig.maxFee > 0n).to.be.true;
391-
expect(linearConfig.halfAmount > 0n).to.be.true;
392-
expect(linearConfig.bps).to.equal(8n);
399+
400+
expect(expandedConfig.maxFee > 0n).to.be.true;
401+
expect(expandedConfig.halfAmount > 0n).to.be.true;
402+
expect(expandedConfig.bps).to.equal(8);
393403

394404
const roundTripBps = convertToBps(
395-
linearConfig.maxFee,
396-
linearConfig.halfAmount,
405+
expandedConfig.maxFee,
406+
expandedConfig.halfAmount,
397407
);
398-
expect(roundTripBps).to.equal(8n);
408+
expect(roundTripBps).to.equal(8);
399409
});
400410

401411
it('should expand nested RoutingFee config for zero-supply token', async () => {
@@ -412,7 +422,7 @@ describe('EvmTokenFeeModule', () => {
412422
type: TokenFeeType.LinearFee,
413423
owner: signer.address,
414424
token: zeroSupplyToken.address,
415-
bps: 8n,
425+
bps: 8,
416426
},
417427
},
418428
};
@@ -438,7 +448,7 @@ describe('EvmTokenFeeModule', () => {
438448
const linearFee = nestedFee as LinearFeeConfig;
439449
expect(linearFee.maxFee > 0n).to.be.true;
440450
expect(linearFee.halfAmount > 0n).to.be.true;
441-
expect(linearFee.bps).to.equal(8n);
451+
expect(linearFee.bps).to.equal(8);
442452
});
443453

444454
it('should expand config with explicit maxFee/halfAmount (no bps) and preserve values', async () => {
@@ -466,11 +476,10 @@ describe('EvmTokenFeeModule', () => {
466476
expandedConfig.type === TokenFeeType.LinearFee,
467477
`Must be ${TokenFeeType.LinearFee}`,
468478
);
469-
const linearConfig = expandedConfig as LinearFeeConfig;
470479

471-
expect(linearConfig.maxFee).to.equal(explicitMaxFee);
472-
expect(linearConfig.halfAmount).to.equal(explicitHalfAmount);
473-
expect(linearConfig.bps).to.equal(expectedBps);
480+
expect(expandedConfig.maxFee).to.equal(explicitMaxFee);
481+
expect(expandedConfig.halfAmount).to.equal(explicitHalfAmount);
482+
expect(expandedConfig.bps).to.equal(expectedBps);
474483
});
475484

476485
it('should expand nested RoutingFee with explicit maxFee/halfAmount in child LinearFee', async () => {
@@ -525,7 +534,7 @@ describe('EvmTokenFeeModule', () => {
525534
[test4Chain]: {
526535
type: TokenFeeType.LinearFee,
527536
owner: signer.address,
528-
bps: 8n,
537+
bps: 8,
529538
},
530539
},
531540
} as ResolvedTokenFeeConfigInput;
@@ -545,5 +554,32 @@ describe('EvmTokenFeeModule', () => {
545554
assert(nestedFee, 'Nested fee must exist');
546555
expect(nestedFee.token).to.equal(token.address);
547556
});
557+
558+
it('should expand config with fractional bps (1.5)', async () => {
559+
const reader = new EvmTokenFeeReader(multiProvider, test4Chain);
560+
const expected = reader.convertFromBps(1.5);
561+
562+
const inputConfig: ResolvedTokenFeeConfigInput = {
563+
type: TokenFeeType.LinearFee,
564+
owner: signer.address,
565+
token: token.address,
566+
bps: 1.5,
567+
};
568+
569+
const expandedConfig = await EvmTokenFeeModule.expandConfig({
570+
config: inputConfig,
571+
multiProvider,
572+
chainName: test4Chain,
573+
});
574+
575+
assert(
576+
expandedConfig.type === TokenFeeType.LinearFee,
577+
`Must be ${TokenFeeType.LinearFee}`,
578+
);
579+
580+
expect(expandedConfig.maxFee).to.equal(expected.maxFee);
581+
expect(expandedConfig.halfAmount).to.equal(expected.halfAmount);
582+
expect(expandedConfig.bps).to.equal(1.5);
583+
});
548584
});
549585
});

typescript/sdk/src/fee/EvmTokenFeeModule.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ export class EvmTokenFeeModule extends HyperlaneModule<
149149

150150
let maxFee: bigint;
151151
let halfAmount: bigint;
152-
let bps: bigint;
152+
let bps: number;
153153

154154
const reader = new EvmTokenFeeReader(
155155
params.multiProvider,

0 commit comments

Comments
 (0)