Skip to content

Commit

Permalink
feat: unenroll warp routes (#4957)
Browse files Browse the repository at this point in the history
### Description
Add feature to unenroll warp routes using `warp apply`

### Drive-by changes
Update enroll warp route to use `difference()` similar to ICA enrollment

### Backward compatibility
Yes

### Testing
Manual/Unit Tests
  • Loading branch information
ltyu authored Dec 5, 2024
1 parent 9a09afc commit a96448f
Show file tree
Hide file tree
Showing 3 changed files with 134 additions and 45 deletions.
5 changes: 5 additions & 0 deletions .changeset/fifty-meals-appear.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@hyperlane-xyz/sdk': minor
---

Add logic into SDK to enable warp route unenrollment
62 changes: 52 additions & 10 deletions typescript/sdk/src/token/EvmERC20WarpModule.hardhat-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
proxyAdmin,
serializeContracts,
} from '@hyperlane-xyz/sdk';
import { randomInt } from '@hyperlane-xyz/utils';

import { TestCoreApp } from '../core/TestCoreApp.js';
import { TestCoreDeployer } from '../core/TestCoreDeployer.js';
Expand Down Expand Up @@ -510,7 +511,7 @@ describe('EvmERC20WarpHyperlaneModule', async () => {
);
});

it('should update connected routers', async () => {
it('should enroll connected routers', async () => {
const config = {
...baseConfig,
type: TokenType.native,
Expand All @@ -527,7 +528,7 @@ describe('EvmERC20WarpHyperlaneModule', async () => {
multiProvider,
proxyFactoryFactories: ismFactoryAddresses,
});
const numOfRouters = Math.floor(Math.random() * 10);
const numOfRouters = randomInt(10, 0);
await sendTxs(
await evmERC20WarpModule.update({
...config,
Expand All @@ -541,7 +542,44 @@ describe('EvmERC20WarpHyperlaneModule', async () => {
);
});

it('should only extend routers if they are new ones are different', async () => {
it('should unenroll connected routers', async () => {
const config = {
...baseConfig,
type: TokenType.native,
ismFactoryAddresses,
} as TokenRouterConfig;

// Deploy using WarpModule
const evmERC20WarpModule = await EvmERC20WarpModule.create({
chain,
config: {
...config,
interchainSecurityModule: ismAddress,
},
multiProvider,
proxyFactoryFactories: ismFactoryAddresses,
});
const numOfRouters = randomInt(10, 0);
await sendTxs(
await evmERC20WarpModule.update({
...config,
remoteRouters: randomRemoteRouters(numOfRouters),
}),
);
// Read config & delete remoteRouters
const existingConfig = await evmERC20WarpModule.read();
for (let i = 0; i < numOfRouters; i++) {
delete existingConfig.remoteRouters?.[i.toString()];
await sendTxs(await evmERC20WarpModule.update(existingConfig));

const updatedConfig = await evmERC20WarpModule.read();
expect(Object.keys(updatedConfig.remoteRouters!).length).to.be.equal(
numOfRouters - (i + 1),
);
}
});

it('should replace an enrollment if they are new one different, if the config lengths are the same', async () => {
const config = {
...baseConfig,
type: TokenType.native,
Expand Down Expand Up @@ -579,20 +617,24 @@ describe('EvmERC20WarpHyperlaneModule', async () => {
await sendTxs(txs);

// Try to extend with the different remoteRouters, but same length
const extendedRemoteRouter = {
3: {
address: randomAddress(),
},
};
txs = await evmERC20WarpModule.update({
...config,
remoteRouters: {
3: {
address: randomAddress(),
},
},
remoteRouters: extendedRemoteRouter,
});

expect(txs.length).to.equal(1);
expect(txs.length).to.equal(2);
await sendTxs(txs);

updatedConfig = await evmERC20WarpModule.read();
expect(Object.keys(updatedConfig.remoteRouters!).length).to.be.equal(2);
expect(Object.keys(updatedConfig.remoteRouters!).length).to.be.equal(1);
expect(updatedConfig.remoteRouters?.['3'].address.toLowerCase()).to.be.eq(
extendedRemoteRouter['3'].address.toLowerCase(),
);
});

it('should update the owner only if they are different', async () => {
Expand Down
112 changes: 77 additions & 35 deletions typescript/sdk/src/token/EvmERC20WarpModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
addressToBytes32,
assert,
deepEquals,
difference,
eqAddress,
isObjEmpty,
objMap,
Expand All @@ -39,7 +40,6 @@ import { DerivedIsmConfig } from '../ism/EvmIsmReader.js';
import { MultiProvider } from '../providers/MultiProvider.js';
import { AnnotatedEV5Transaction } from '../providers/ProviderType.js';
import { ChainName, ChainNameOrId } from '../types.js';
import { normalizeConfig } from '../utils/ism.js';

import { EvmERC20WarpRouteReader } from './EvmERC20WarpRouteReader.js';
import { HypERC20Deployer } from './deploy.js';
Expand Down Expand Up @@ -115,7 +115,11 @@ export class EvmERC20WarpModule extends HyperlaneModule<
transactions.push(
...(await this.createIsmUpdateTxs(actualConfig, expectedConfig)),
...(await this.createHookUpdateTxs(actualConfig, expectedConfig)),
...this.createRemoteRoutersUpdateTxs(actualConfig, expectedConfig),
...this.createEnrollRemoteRoutersUpdateTxs(actualConfig, expectedConfig),
...this.createUnenrollRemoteRoutersUpdateTxs(
actualConfig,
expectedConfig,
),
...this.createSetDestinationGasUpdateTxs(actualConfig, expectedConfig),
...this.createOwnershipUpdateTxs(actualConfig, expectedConfig),
...proxyAdminUpdateTxs(
Expand All @@ -136,7 +140,7 @@ export class EvmERC20WarpModule extends HyperlaneModule<
* @param expectedConfig - The expected token router configuration.
* @returns A array with a single Ethereum transaction that need to be executed to enroll the routers
*/
createRemoteRoutersUpdateTxs(
createEnrollRemoteRoutersUpdateTxs(
actualConfig: TokenRouterConfig,
expectedConfig: TokenRouterConfig,
): AnnotatedEV5Transaction[] {
Expand All @@ -148,46 +152,84 @@ export class EvmERC20WarpModule extends HyperlaneModule<
assert(actualConfig.remoteRouters, 'actualRemoteRouters is undefined');
assert(expectedConfig.remoteRouters, 'actualRemoteRouters is undefined');

// We normalize the addresses for comparison
actualConfig.remoteRouters = Object.fromEntries(
Object.entries(actualConfig.remoteRouters).map(([key, value]) => [
key,
// normalizeConfig removes the address property but we don't want to lose that info
{ ...normalizeConfig(value), address: normalizeConfig(value.address) },
]),
const { remoteRouters: actualRemoteRouters } = actualConfig;
const { remoteRouters: expectedRemoteRouters } = expectedConfig;

const routesToEnroll = Array.from(
difference(
new Set(Object.keys(expectedRemoteRouters)),
new Set(Object.keys(actualRemoteRouters)),
),
);
expectedConfig.remoteRouters = Object.fromEntries(
Object.entries(expectedConfig.remoteRouters).map(([key, value]) => [
key,
// normalizeConfig removes the address property but we don't want to lose that info
{ ...normalizeConfig(value), address: normalizeConfig(value.address) },
]),

if (routesToEnroll.length === 0) {
return updateTransactions;
}

const contractToUpdate = TokenRouter__factory.connect(
this.args.addresses.deployedTokenRoute,
this.multiProvider.getProvider(this.domainId),
);

updateTransactions.push({
chainId: this.chainId,
annotation: `Enrolling Router ${this.args.addresses.deployedTokenRoute} on ${this.args.chain}`,
to: contractToUpdate.address,
data: contractToUpdate.interface.encodeFunctionData(
'enrollRemoteRouters',
[
routesToEnroll.map((k) => Number(k)),
routesToEnroll.map((a) =>
addressToBytes32(expectedRemoteRouters[a].address),
),
],
),
});

return updateTransactions;
}

createUnenrollRemoteRoutersUpdateTxs(
actualConfig: TokenRouterConfig,
expectedConfig: TokenRouterConfig,
): AnnotatedEV5Transaction[] {
const updateTransactions: AnnotatedEV5Transaction[] = [];
if (!expectedConfig.remoteRouters) {
return [];
}

assert(actualConfig.remoteRouters, 'actualRemoteRouters is undefined');
assert(expectedConfig.remoteRouters, 'actualRemoteRouters is undefined');

const { remoteRouters: actualRemoteRouters } = actualConfig;
const { remoteRouters: expectedRemoteRouters } = expectedConfig;

if (!deepEquals(actualRemoteRouters, expectedRemoteRouters)) {
const contractToUpdate = TokenRouter__factory.connect(
this.args.addresses.deployedTokenRoute,
this.multiProvider.getProvider(this.domainId),
);
const routesToUnenroll = Array.from(
difference(
new Set(Object.keys(actualRemoteRouters)),
new Set(Object.keys(expectedRemoteRouters)),
),
);

updateTransactions.push({
chainId: this.chainId,
annotation: `Enrolling Router ${this.args.addresses.deployedTokenRoute} on ${this.args.chain}`,
to: contractToUpdate.address,
data: contractToUpdate.interface.encodeFunctionData(
'enrollRemoteRouters',
[
Object.keys(expectedRemoteRouters).map((k) => Number(k)),
Object.values(expectedRemoteRouters).map((a) =>
addressToBytes32(a.address),
),
],
),
});
if (routesToUnenroll.length === 0) {
return updateTransactions;
}

const contractToUpdate = TokenRouter__factory.connect(
this.args.addresses.deployedTokenRoute,
this.multiProvider.getProvider(this.domainId),
);

updateTransactions.push({
annotation: `Unenrolling Router ${this.args.addresses.deployedTokenRoute} on ${this.args.chain}`,
chainId: this.chainId,
to: contractToUpdate.address,
data: contractToUpdate.interface.encodeFunctionData(
'unenrollRemoteRouters(uint32[])',
[routesToUnenroll.map((k) => Number(k))],
),
});

return updateTransactions;
}

Expand Down

0 comments on commit a96448f

Please sign in to comment.