Skip to content

Commit de5f6b5

Browse files
authored
fix(sdk): fix evm warp read (#8325)
1 parent 87c9689 commit de5f6b5

3 files changed

Lines changed: 200 additions & 5 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@hyperlane-xyz/sdk": patch
3+
---
4+
5+
Fixed fetchScale version gate to compare against the contract version where scaling was first introduced (6.0.0) instead of the fraction scaling version (11.0.0), preventing failed scale() reads on pre-scaling contracts.

typescript/sdk/src/token/EvmWarpRouteReader.hardhat-test.ts

Lines changed: 182 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1247,10 +1247,188 @@ describe('EvmWarpRouteReader', async () => {
12471247
});
12481248
}
12491249

1250-
// Note: legacy contract fetchScale path (< 11.0.0) cannot be tested in hardhat
1251-
// without deploying a real legacy contract. The legacy path converts a single
1252-
// uint256 scale() return to { numerator: bigint, denominator: 1n }.
1253-
// Coverage is provided by the fetchScale stubs in other tests returning NormalizedScale.
1250+
// Note: legacy contract fetchScale path (< 11.0.0) is tested via hardhat_setCode
1251+
// to inject minimal bytecode that responds to the scale() selector.
1252+
// The legacy path converts a single uint256 scale() return to { numerator: bigint, denominator: 1n }.
1253+
1254+
describe('fetchScale', () => {
1255+
it('should return undefined for contracts before scaling was introduced (< 6.0.0)', async () => {
1256+
const config: WarpRouteDeployConfigMailboxRequired = {
1257+
[chain]: {
1258+
type: TokenType.synthetic,
1259+
name: TOKEN_NAME,
1260+
symbol: TOKEN_NAME,
1261+
decimals: TOKEN_DECIMALS,
1262+
hook: await mailbox.defaultHook(),
1263+
...baseConfig,
1264+
},
1265+
};
1266+
1267+
const warpRoute = await deployer.deploy(config);
1268+
const warpAddress = warpRoute[chain].synthetic.address;
1269+
1270+
const fetchPackageVersionStub = sinon
1271+
.stub(evmERC20WarpRouteReader, 'fetchPackageVersion')
1272+
.resolves('5.0.0');
1273+
1274+
const result = await evmERC20WarpRouteReader.fetchScale(warpAddress);
1275+
expect(result).to.be.undefined;
1276+
1277+
fetchPackageVersionStub.restore();
1278+
});
1279+
1280+
it('should read legacy scale() for contracts with version >= 6.0.0 and < 11.0.0', async () => {
1281+
const expectedScale = 1000n;
1282+
1283+
// Deploy a minimal contract that returns expectedScale for scale()
1284+
// Bytecode: PUSH32 <value> PUSH1 0x00 MSTORE PUSH1 0x20 PUSH1 0x00 RETURN
1285+
const encodedScale = expectedScale.toString(16).padStart(64, '0');
1286+
const runtimeBytecode = `0x7f${encodedScale}60005260206000f3`;
1287+
const mockAddress = '0x' + 'ab'.repeat(20);
1288+
await hre.network.provider.send('hardhat_setCode', [
1289+
mockAddress,
1290+
runtimeBytecode,
1291+
]);
1292+
1293+
const fetchPackageVersionStub = sinon
1294+
.stub(evmERC20WarpRouteReader, 'fetchPackageVersion')
1295+
.resolves('9.0.0');
1296+
1297+
const result = await evmERC20WarpRouteReader.fetchScale(mockAddress);
1298+
expect(result).to.deep.equal({
1299+
numerator: expectedScale,
1300+
denominator: 1n,
1301+
});
1302+
1303+
fetchPackageVersionStub.restore();
1304+
});
1305+
1306+
it('should read scaleNumerator/scaleDenominator for contracts >= 11.0.0', async () => {
1307+
const scaleNumerator = 1;
1308+
const scaleDenominator = 1000000000000;
1309+
const config: WarpRouteDeployConfigMailboxRequired = {
1310+
[chain]: {
1311+
type: TokenType.synthetic,
1312+
name: TOKEN_NAME,
1313+
symbol: TOKEN_NAME,
1314+
decimals: 6,
1315+
scale: {
1316+
numerator: scaleNumerator,
1317+
denominator: scaleDenominator,
1318+
},
1319+
hook: await mailbox.defaultHook(),
1320+
...baseConfig,
1321+
},
1322+
};
1323+
1324+
const warpRoute = await deployer.deploy(config);
1325+
const warpAddress = warpRoute[chain].synthetic.address;
1326+
1327+
const result = await evmERC20WarpRouteReader.fetchScale(warpAddress);
1328+
expect(result).to.deep.equal({
1329+
numerator: BigInt(scaleNumerator),
1330+
denominator: BigInt(scaleDenominator),
1331+
});
1332+
});
1333+
1334+
it('should read legacy scale() at the exact boundary version 6.0.0', async () => {
1335+
const expectedScale = 500n;
1336+
1337+
const encodedScale = expectedScale.toString(16).padStart(64, '0');
1338+
const runtimeBytecode = `0x7f${encodedScale}60005260206000f3`;
1339+
const mockAddress = '0x' + 'ac'.repeat(20);
1340+
await hre.network.provider.send('hardhat_setCode', [
1341+
mockAddress,
1342+
runtimeBytecode,
1343+
]);
1344+
1345+
const fetchPackageVersionStub = sinon
1346+
.stub(evmERC20WarpRouteReader, 'fetchPackageVersion')
1347+
.resolves('6.0.0');
1348+
1349+
const result = await evmERC20WarpRouteReader.fetchScale(mockAddress);
1350+
expect(result).to.deep.equal({
1351+
numerator: expectedScale,
1352+
denominator: 1n,
1353+
});
1354+
1355+
fetchPackageVersionStub.restore();
1356+
});
1357+
1358+
it('should read scaleNumerator/scaleDenominator at the exact boundary version 11.0.0', async () => {
1359+
const scaleNumerator = 1;
1360+
const scaleDenominator = 1000000000000;
1361+
const config: WarpRouteDeployConfigMailboxRequired = {
1362+
[chain]: {
1363+
type: TokenType.synthetic,
1364+
name: TOKEN_NAME,
1365+
symbol: TOKEN_NAME,
1366+
decimals: 6,
1367+
scale: {
1368+
numerator: scaleNumerator,
1369+
denominator: scaleDenominator,
1370+
},
1371+
hook: await mailbox.defaultHook(),
1372+
...baseConfig,
1373+
},
1374+
};
1375+
1376+
const warpRoute = await deployer.deploy(config);
1377+
const warpAddress = warpRoute[chain].synthetic.address;
1378+
1379+
const fetchPackageVersionStub = sinon
1380+
.stub(evmERC20WarpRouteReader, 'fetchPackageVersion')
1381+
.resolves('11.0.0');
1382+
1383+
const result = await evmERC20WarpRouteReader.fetchScale(warpAddress);
1384+
expect(result).to.deep.equal({
1385+
numerator: BigInt(scaleNumerator),
1386+
denominator: BigInt(scaleDenominator),
1387+
});
1388+
1389+
fetchPackageVersionStub.restore();
1390+
});
1391+
1392+
it('should return undefined for legacy identity scale (scale() = 1)', async () => {
1393+
const identityScale = 1n;
1394+
1395+
const encodedScale = identityScale.toString(16).padStart(64, '0');
1396+
const runtimeBytecode = `0x7f${encodedScale}60005260206000f3`;
1397+
const mockAddress = '0x' + 'ad'.repeat(20);
1398+
await hre.network.provider.send('hardhat_setCode', [
1399+
mockAddress,
1400+
runtimeBytecode,
1401+
]);
1402+
1403+
const fetchPackageVersionStub = sinon
1404+
.stub(evmERC20WarpRouteReader, 'fetchPackageVersion')
1405+
.resolves('9.0.0');
1406+
1407+
const result = await evmERC20WarpRouteReader.fetchScale(mockAddress);
1408+
expect(result).to.be.undefined;
1409+
1410+
fetchPackageVersionStub.restore();
1411+
});
1412+
1413+
it('should return undefined for identity scale (1/1) on >= 11.0.0 contracts', async () => {
1414+
const config: WarpRouteDeployConfigMailboxRequired = {
1415+
[chain]: {
1416+
type: TokenType.synthetic,
1417+
name: TOKEN_NAME,
1418+
symbol: TOKEN_NAME,
1419+
decimals: TOKEN_DECIMALS,
1420+
hook: await mailbox.defaultHook(),
1421+
...baseConfig,
1422+
},
1423+
};
1424+
1425+
const warpRoute = await deployer.deploy(config);
1426+
const warpAddress = warpRoute[chain].synthetic.address;
1427+
1428+
const result = await evmERC20WarpRouteReader.fetchScale(warpAddress);
1429+
expect(result).to.be.undefined;
1430+
});
1431+
});
12541432

12551433
it('should fail when modern version contract claims v10.0.0+ but is missing token() method', async () => {
12561434
const config: WarpRouteDeployConfigMailboxRequired = {

typescript/sdk/src/token/EvmWarpRouteReader.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,14 @@ import { getExtraLockBoxConfigs } from './xerc20.js';
8585

8686
const REBALANCING_CONTRACT_VERSION = '8.0.0';
8787
export const TOKEN_FEE_CONTRACT_VERSION = '10.0.0';
88+
89+
// version that introduced the fractional scale interface
8890
const SCALE_FRACTION_VERSION = '11.0.0';
91+
92+
// version that introduced the legacy scale interface
93+
// https://github.com/hyperlane-xyz/hyperlane-monorepo/releases/tag/%40hyperlane-xyz%2Fcore%406.0.0
94+
const SCALE_VERSION = '6.0.0';
95+
8996
// Version that first introduced ppm precision for CCTP V2 fee storage (was bps before)
9097
export const CCTP_PPM_STORAGE_VERSION = '10.2.0';
9198
// Version that renamed maxFeeBps() to maxFeePpm() on-chain
@@ -1229,14 +1236,19 @@ export class EvmWarpRouteReader extends EvmRouterReader {
12291236
const packageVersion = await this.fetchPackageVersion(tokenRouterAddress);
12301237
const hasScaleFractionInterface =
12311238
compareVersions(packageVersion, SCALE_FRACTION_VERSION) >= 0;
1239+
const hasScaleInterface =
1240+
compareVersions(packageVersion, SCALE_VERSION) >= 0;
1241+
1242+
if (!hasScaleFractionInterface && !hasScaleInterface) {
1243+
return;
1244+
}
12321245

12331246
const tokenRouter = TokenRouter__factory.connect(
12341247
tokenRouterAddress,
12351248
this.provider,
12361249
);
12371250

12381251
let result: NormalizedScale;
1239-
12401252
if (hasScaleFractionInterface) {
12411253
// Read new format (scaleNumerator and scaleDenominator)
12421254
const [numerator, denominator] = await Promise.all([

0 commit comments

Comments
 (0)