diff --git a/.changeset/plenty-chicken-clean.md b/.changeset/plenty-chicken-clean.md new file mode 100644 index 000000000..e35a59eff --- /dev/null +++ b/.changeset/plenty-chicken-clean.md @@ -0,0 +1,6 @@ +--- +'@hyperlane-xyz/cli': minor +'@hyperlane-xyz/sdk': minor +--- + +Add rebasing yield route support into CLI/SDK diff --git a/typescript/cli/package.json b/typescript/cli/package.json index 19421e2e9..eb1403de0 100644 --- a/typescript/cli/package.json +++ b/typescript/cli/package.json @@ -25,12 +25,14 @@ "devDependencies": { "@ethersproject/abi": "*", "@ethersproject/providers": "*", + "@types/chai-as-promised": "^8", "@types/mocha": "^10.0.1", "@types/node": "^18.14.5", "@types/yargs": "^17.0.24", "@typescript-eslint/eslint-plugin": "^7.4.0", "@typescript-eslint/parser": "^7.4.0", - "chai": "4.5.0", + "chai": "^4.5.0", + "chai-as-promised": "^8.0.0", "eslint": "^8.57.0", "eslint-config-prettier": "^9.1.0", "mocha": "^10.2.0", diff --git a/typescript/cli/src/config/warp.ts b/typescript/cli/src/config/warp.ts index 6ce1f6665..29246cd33 100644 --- a/typescript/cli/src/config/warp.ts +++ b/typescript/cli/src/config/warp.ts @@ -34,12 +34,15 @@ import { createAdvancedIsmConfig } from './ism.js'; const TYPE_DESCRIPTIONS: Record = { [TokenType.synthetic]: 'A new ERC20 with remote transfer functionality', + [TokenType.syntheticRebase]: `A rebasing ERC20 with remote transfer functionality. Must be paired with ${TokenType.collateralVaultRebase}`, [TokenType.collateral]: 'Extends an existing ERC20 with remote transfer functionality', [TokenType.native]: 'Extends the native token with remote transfer functionality', [TokenType.collateralVault]: - 'Extends an existing ERC4626 with remote transfer functionality', + 'Extends an existing ERC4626 with remote transfer functionality. Yields are manually claimed by owner.', + [TokenType.collateralVaultRebase]: + 'Extends an existing ERC4626 with remote transfer functionality. Rebases yields to token holders.', [TokenType.collateralFiat]: 'Extends an existing FiatToken with remote transfer functionality', [TokenType.XERC20]: @@ -129,6 +132,7 @@ export async function createWarpRouteDeployConfig({ ); const result: WarpRouteDeployConfig = {}; + let typeChoices = TYPE_CHOICES; for (const chain of warpChains) { logBlue(`${chain}: Configuring warp route...`); @@ -167,7 +171,7 @@ export async function createWarpRouteDeployConfig({ const type = await select({ message: `Select ${chain}'s token type`, - choices: TYPE_CHOICES, + choices: typeChoices, }); // TODO: restore NFT prompting @@ -192,6 +196,34 @@ export async function createWarpRouteDeployConfig({ }), }; break; + case TokenType.syntheticRebase: + result[chain] = { + mailbox, + type, + owner, + isNft, + collateralChainName: '', // This will be derived correctly by zod.parse() below + interchainSecurityModule, + }; + typeChoices = restrictChoices([ + TokenType.syntheticRebase, + TokenType.collateralVaultRebase, + ]); + break; + case TokenType.collateralVaultRebase: + result[chain] = { + mailbox, + type, + owner, + isNft, + interchainSecurityModule, + token: await input({ + message: `Enter the ERC-4626 vault address on chain ${chain}`, + }), + }; + + typeChoices = restrictChoices([TokenType.syntheticRebase]); + break; case TokenType.collateralVault: result[chain] = { mailbox, @@ -229,6 +261,10 @@ export async function createWarpRouteDeployConfig({ } } +function restrictChoices(typeChoices: TokenType[]) { + return TYPE_CHOICES.filter((choice) => typeChoices.includes(choice.name)); +} + // Note, this is different than the function above which reads a config // for a DEPLOYMENT. This gets a config for using a warp route (aka WarpCoreConfig) export function readWarpCoreConfig(filePath: string): WarpCoreConfig { diff --git a/typescript/cli/src/send/transfer.ts b/typescript/cli/src/send/transfer.ts index a2f56ef29..a89eb6aa9 100644 --- a/typescript/cli/src/send/transfer.ts +++ b/typescript/cli/src/send/transfer.ts @@ -23,6 +23,10 @@ import { indentYamlOrJson } from '../utils/files.js'; import { stubMerkleTreeConfig } from '../utils/relay.js'; import { runTokenSelectionStep } from '../utils/tokens.js'; +export const WarpSendLogs = { + SUCCESS: 'Transfer was self-relayed!', +}; + export async function sendTestTransfer({ context, warpCoreConfig, @@ -183,7 +187,7 @@ async function executeDelivery({ log('Attempting self-relay of transfer...'); await relayer.relayMessage(transferTxReceipt, messageIndex, message); - logGreen('Transfer was self-relayed!'); + logGreen(WarpSendLogs.SUCCESS); return; } diff --git a/typescript/cli/src/tests/commands/helpers.ts b/typescript/cli/src/tests/commands/helpers.ts index 2d2be7a6e..c4ad03651 100644 --- a/typescript/cli/src/tests/commands/helpers.ts +++ b/typescript/cli/src/tests/commands/helpers.ts @@ -1,3 +1,4 @@ +import { ERC20Test__factory, ERC4626Test__factory } from '@hyperlane-xyz/core'; import { ChainAddresses } from '@hyperlane-xyz/registry'; import { TokenRouterConfig, @@ -10,7 +11,11 @@ import { getContext } from '../../context/context.js'; import { readYamlOrJson, writeYamlOrJson } from '../../utils/files.js'; import { hyperlaneCoreDeploy } from './core.js'; -import { hyperlaneWarpApply, readWarpConfig } from './warp.js'; +import { + hyperlaneWarpApply, + hyperlaneWarpSendRelay, + readWarpConfig, +} from './warp.js'; export const TEST_CONFIGS_PATH = './test-configs'; export const REGISTRY_PATH = `${TEST_CONFIGS_PATH}/anvil`; @@ -123,3 +128,54 @@ export async function getChainId(chainName: string, key: string) { const chainMetadata = await registry.getChainMetadata(chainName); return String(chainMetadata?.chainId); } + +export async function deployToken(privateKey: string, chain: string) { + const { multiProvider } = await getContext({ + registryUri: REGISTRY_PATH, + registryOverrideUri: '', + key: privateKey, + }); + + const token = await new ERC20Test__factory( + multiProvider.getSigner(chain), + ).deploy('token', 'token', '100000000000000000000', 18); + await token.deployed(); + + return token; +} + +export async function deploy4626Vault( + privateKey: string, + chain: string, + tokenAddress: string, +) { + const { multiProvider } = await getContext({ + registryUri: REGISTRY_PATH, + registryOverrideUri: '', + key: privateKey, + }); + + const vault = await new ERC4626Test__factory( + multiProvider.getSigner(chain), + ).deploy(tokenAddress, 'VAULT', 'VAULT'); + await vault.deployed(); + + return vault; +} + +/** + * Performs a round-trip warp relay between two chains using the specified warp core config. + * + * @param chain1 - The first chain to send the warp relay from. + * @param chain2 - The second chain to send the warp relay to and back from. + * @param warpCoreConfigPath - The path to the warp core config file. + * @returns A promise that resolves when the round-trip warp relay is complete. + */ +export async function sendWarpRouteMessageRoundTrip( + chain1: string, + chain2: string, + warpCoreConfigPath: string, +) { + await hyperlaneWarpSendRelay(chain1, chain2, warpCoreConfigPath); + return hyperlaneWarpSendRelay(chain2, chain1, warpCoreConfigPath); +} diff --git a/typescript/cli/src/tests/commands/warp.ts b/typescript/cli/src/tests/commands/warp.ts index 16b8acbfe..3f3eec338 100644 --- a/typescript/cli/src/tests/commands/warp.ts +++ b/typescript/cli/src/tests/commands/warp.ts @@ -12,8 +12,10 @@ $.verbose = true; * Deploys the Warp route to the specified chain using the provided config. */ export async function hyperlaneWarpDeploy(warpCorePath: string) { + // --overrides is " " to allow local testing to work return $`yarn workspace @hyperlane-xyz/cli run hyperlane warp deploy \ --registry ${REGISTRY_PATH} \ + --overrides " " \ --config ${warpCorePath} \ --key ${ANVIL_KEY} \ --verbosity debug \ @@ -30,6 +32,7 @@ export async function hyperlaneWarpApply( ) { return $`yarn workspace @hyperlane-xyz/cli run hyperlane warp apply \ --registry ${REGISTRY_PATH} \ + --overrides " " \ --config ${warpDeployPath} \ --warp ${warpCorePath} \ --key ${ANVIL_KEY} \ @@ -45,6 +48,7 @@ export async function hyperlaneWarpRead( ) { return $`yarn workspace @hyperlane-xyz/cli run hyperlane warp read \ --registry ${REGISTRY_PATH} \ + --overrides " " \ --address ${warpAddress} \ --chain ${chain} \ --key ${ANVIL_KEY} \ @@ -52,6 +56,23 @@ export async function hyperlaneWarpRead( --config ${warpDeployPath}`; } +export async function hyperlaneWarpSendRelay( + origin: string, + destination: string, + warpCorePath: string, +) { + return $`yarn workspace @hyperlane-xyz/cli run hyperlane warp send \ + --relay \ + --registry ${REGISTRY_PATH} \ + --overrides " " \ + --origin ${origin} \ + --destination ${destination} \ + --warp ${warpCorePath} \ + --key ${ANVIL_KEY} \ + --verbosity debug \ + --yes`; +} + /** * Reads the Warp route deployment config to specified output path. * @param warpCorePath path to warp core diff --git a/typescript/cli/src/tests/warp-apply.e2e-test.ts b/typescript/cli/src/tests/warp-apply.e2e-test.ts index 7b0c20d91..2346038d6 100644 --- a/typescript/cli/src/tests/warp-apply.e2e-test.ts +++ b/typescript/cli/src/tests/warp-apply.e2e-test.ts @@ -36,7 +36,7 @@ const TEMP_PATH = '/tmp'; // /temp gets removed at the end of all-test.sh const WARP_CONFIG_PATH_2 = `${TEMP_PATH}/anvil2/warp-route-deployment-anvil2.yaml`; const WARP_CORE_CONFIG_PATH_2 = `${REGISTRY_PATH}/deployments/warp_routes/ETH/anvil2-config.yaml`; -const TEST_TIMEOUT = 60_000; // Long timeout since these tests can take a while +const TEST_TIMEOUT = 100_000; // Long timeout since these tests can take a while describe('WarpApply e2e tests', async function () { let chain2Addresses: ChainAddresses = {}; this.timeout(TEST_TIMEOUT); diff --git a/typescript/cli/src/tests/warp-deploy.e2e-test.ts b/typescript/cli/src/tests/warp-deploy.e2e-test.ts new file mode 100644 index 000000000..6263f70cb --- /dev/null +++ b/typescript/cli/src/tests/warp-deploy.e2e-test.ts @@ -0,0 +1,114 @@ +import * as chai from 'chai'; +import chaiAsPromised from 'chai-as-promised'; + +import { ChainAddresses } from '@hyperlane-xyz/registry'; +import { TokenType, WarpRouteDeployConfig } from '@hyperlane-xyz/sdk'; + +import { WarpSendLogs } from '../send/transfer.js'; +import { writeYamlOrJson } from '../utils/files.js'; + +import { + ANVIL_KEY, + REGISTRY_PATH, + deploy4626Vault, + deployOrUseExistingCore, + deployToken, + sendWarpRouteMessageRoundTrip, +} from './commands/helpers.js'; +import { hyperlaneWarpDeploy, readWarpConfig } from './commands/warp.js'; + +chai.use(chaiAsPromised); +const expect = chai.expect; +chai.should(); + +const CHAIN_NAME_2 = 'anvil2'; +const CHAIN_NAME_3 = 'anvil3'; + +const EXAMPLES_PATH = './examples'; +const TEMP_PATH = '/tmp'; // /temp gets removed at the end of all-test.sh + +const CORE_CONFIG_PATH = `${EXAMPLES_PATH}/core-config.yaml`; +const WARP_CONFIG_PATH = `${TEMP_PATH}/warp-route-deployment-deploy.yaml`; +const WARP_CORE_CONFIG_PATH_2_3 = `${REGISTRY_PATH}/deployments/warp_routes/VAULT/anvil2-anvil3-config.yaml`; + +const TEST_TIMEOUT = 60_000; // Long timeout since these tests can take a while +describe('WarpDeploy e2e tests', async function () { + let chain2Addresses: ChainAddresses = {}; + let token: any; + let vault: any; + + this.timeout(TEST_TIMEOUT); + + before(async function () { + chain2Addresses = await deployOrUseExistingCore( + CHAIN_NAME_2, + CORE_CONFIG_PATH, + ANVIL_KEY, + ); + + await deployOrUseExistingCore(CHAIN_NAME_3, CORE_CONFIG_PATH, ANVIL_KEY); + + token = await deployToken(ANVIL_KEY, CHAIN_NAME_2); + vault = await deploy4626Vault(ANVIL_KEY, CHAIN_NAME_2, token.address); + }); + + it('should only allow rebasing yield route to be deployed with rebasing synthetic', async function () { + const warpConfig: WarpRouteDeployConfig = { + [CHAIN_NAME_2]: { + type: TokenType.collateralVaultRebase, + token: vault.address, + mailbox: chain2Addresses.mailbox, + owner: chain2Addresses.mailbox, + }, + [CHAIN_NAME_3]: { + type: TokenType.synthetic, + mailbox: chain2Addresses.mailbox, + owner: chain2Addresses.mailbox, + }, + }; + + writeYamlOrJson(WARP_CONFIG_PATH, warpConfig); + await hyperlaneWarpDeploy(WARP_CONFIG_PATH).should.be.rejected; // TODO: revisit this to figure out how to parse the error. + }); + + it(`should be able to bridge between ${TokenType.collateralVaultRebase} and ${TokenType.syntheticRebase}`, async function () { + const warpConfig: WarpRouteDeployConfig = { + [CHAIN_NAME_2]: { + type: TokenType.collateralVaultRebase, + token: vault.address, + mailbox: chain2Addresses.mailbox, + owner: chain2Addresses.mailbox, + }, + [CHAIN_NAME_3]: { + type: TokenType.syntheticRebase, + mailbox: chain2Addresses.mailbox, + owner: chain2Addresses.mailbox, + collateralChainName: CHAIN_NAME_2, + }, + }; + + writeYamlOrJson(WARP_CONFIG_PATH, warpConfig); + await hyperlaneWarpDeploy(WARP_CONFIG_PATH); + + // Check collateralRebase + const collateralRebaseConfig = ( + await readWarpConfig( + CHAIN_NAME_2, + WARP_CORE_CONFIG_PATH_2_3, + WARP_CONFIG_PATH, + ) + )[CHAIN_NAME_2]; + + expect(collateralRebaseConfig.type).to.equal( + TokenType.collateralVaultRebase, + ); + + // Try to send a transaction + const { stdout } = await sendWarpRouteMessageRoundTrip( + CHAIN_NAME_2, + CHAIN_NAME_3, + WARP_CORE_CONFIG_PATH_2_3, + ); + expect(stdout).to.include(WarpSendLogs.SUCCESS); + }); +}); diff --git a/typescript/sdk/src/index.ts b/typescript/sdk/src/index.ts index ee54c0409..e80ad1f3e 100644 --- a/typescript/sdk/src/index.ts +++ b/typescript/sdk/src/index.ts @@ -512,9 +512,11 @@ export { NativeConfig, TokenRouterConfigSchema, WarpRouteDeployConfigSchema, + WarpRouteDeployConfigSchemaErrors, isCollateralConfig, isNativeConfig, isSyntheticConfig, + isSyntheticRebaseConfig, isTokenMetadata, } from './token/schemas.js'; export { isCompliant } from './utils/schemas.js'; diff --git a/typescript/sdk/src/token/EvmERC20WarpModule.hardhat-test.ts b/typescript/sdk/src/token/EvmERC20WarpModule.hardhat-test.ts index 6db166c5b..467a150ad 100644 --- a/typescript/sdk/src/token/EvmERC20WarpModule.hardhat-test.ts +++ b/typescript/sdk/src/token/EvmERC20WarpModule.hardhat-test.ts @@ -111,7 +111,7 @@ describe('EvmERC20WarpHyperlaneModule', async () => { }); it('should create with a collateral config', async () => { - const config = { + const config: TokenRouterConfig = { ...baseConfig, type: TokenType.collateral, token: token.address, @@ -139,7 +139,7 @@ describe('EvmERC20WarpHyperlaneModule', async () => { TOKEN_NAME, TOKEN_NAME, ); - const config = { + const config: TokenRouterConfig = { type: TokenType.collateralVault, token: vault.address, hook: hookAddress, @@ -172,9 +172,8 @@ describe('EvmERC20WarpHyperlaneModule', async () => { }); it('should create with a synthetic config', async () => { - const config = { + const config: TokenRouterConfig = { type: TokenType.synthetic, - token: token.address, hook: hookAddress, name: TOKEN_NAME, symbol: TOKEN_NAME, diff --git a/typescript/sdk/src/token/EvmERC20WarpRouteReader.hardhat-test.ts b/typescript/sdk/src/token/EvmERC20WarpRouteReader.hardhat-test.ts index f086f3b83..c6795fc6c 100644 --- a/typescript/sdk/src/token/EvmERC20WarpRouteReader.hardhat-test.ts +++ b/typescript/sdk/src/token/EvmERC20WarpRouteReader.hardhat-test.ts @@ -14,14 +14,16 @@ import { HyperlaneContractsMap, RouterConfig, TestChainName, - TokenRouterConfig, + WarpRouteDeployConfig, test3, } from '@hyperlane-xyz/sdk'; +import { assert } from '@hyperlane-xyz/utils'; import { TestCoreApp } from '../core/TestCoreApp.js'; import { TestCoreDeployer } from '../core/TestCoreDeployer.js'; import { HyperlaneProxyFactoryDeployer } from '../deploy/HyperlaneProxyFactoryDeployer.js'; import { ProxyFactoryFactories } from '../deploy/contracts.js'; +import { DerivedIsmConfig } from '../ism/EvmIsmReader.js'; import { HyperlaneIsmFactory } from '../ism/HyperlaneIsmFactory.js'; import { MultiProvider } from '../providers/MultiProvider.js'; import { ChainMap } from '../types.js'; @@ -122,12 +124,12 @@ describe('ERC20WarpRouterReader', async () => { it('should derive collateral config correctly', async () => { // Create config - const config = { + const config: WarpRouteDeployConfig = { [chain]: { type: TokenType.collateral, token: token.address, hook: await mailbox.defaultHook(), - interchainsecurityModule: await mailbox.defaultIsm(), + interchainSecurityModule: await mailbox.defaultIsm(), ...baseConfig, }, }; @@ -150,8 +152,10 @@ describe('ERC20WarpRouterReader', async () => { config[chain].hook as string, ), ); - // Check ism. should return undefined - expect(derivedConfig.interchainSecurityModule).to.be.undefined; + // Check ism + expect( + (derivedConfig.interchainSecurityModule as DerivedIsmConfig).address, + ).to.be.equal(await mailbox.defaultIsm()); // Check if token values matches if (derivedConfig.type === TokenType.collateral) { @@ -160,13 +164,45 @@ describe('ERC20WarpRouterReader', async () => { expect(derivedConfig.decimals).to.equal(TOKEN_DECIMALS); } }); + it('should derive synthetic rebase config correctly', async () => { + // Create config + const config: WarpRouteDeployConfig = { + [chain]: { + type: TokenType.syntheticRebase, + collateralChainName: TestChainName.test4, + hook: await mailbox.defaultHook(), + name: TOKEN_NAME, + symbol: TOKEN_NAME, + decimals: TOKEN_DECIMALS, + totalSupply: TOKEN_SUPPLY, + ...baseConfig, + }, + }; + // Deploy with config + const warpRoute = await deployer.deploy(config); + + // Derive config and check if each value matches + const derivedConfig = await evmERC20WarpRouteReader.deriveWarpRouteConfig( + warpRoute[chain].syntheticRebase.address, + ); + for (const [key, value] of Object.entries(derivedConfig)) { + const deployedValue = (config[chain] as any)[key]; + if (deployedValue && typeof value === 'string') + expect(deployedValue).to.equal(value); + } + + // Check if token values matches + if (derivedConfig.type === TokenType.collateral) { + expect(derivedConfig.name).to.equal(TOKEN_NAME); + expect(derivedConfig.symbol).to.equal(TOKEN_NAME); + } + }); it('should derive synthetic config correctly', async () => { // Create config - const config = { + const config: WarpRouteDeployConfig = { [chain]: { type: TokenType.synthetic, - token: token.address, hook: await mailbox.defaultHook(), name: TOKEN_NAME, symbol: TOKEN_NAME, @@ -197,13 +233,13 @@ describe('ERC20WarpRouterReader', async () => { it('should derive native config correctly', async () => { // Create config - const config = { + const config: WarpRouteDeployConfig = { [chain]: { type: TokenType.native, hook: await mailbox.defaultHook(), ...baseConfig, }, - } as ChainMap; + }; // Deploy with config const warpRoute = await deployer.deploy(config); @@ -221,9 +257,61 @@ describe('ERC20WarpRouterReader', async () => { expect(derivedConfig.decimals).to.equal(TOKEN_DECIMALS); }); + it('should derive collateral vault config correctly', async () => { + // Create config + const config: WarpRouteDeployConfig = { + [chain]: { + type: TokenType.collateralVault, + token: vault.address, + ...baseConfig, + }, + }; + // Deploy with config + const warpRoute = await deployer.deploy(config); + // Derive config and check if each value matches + const derivedConfig = await evmERC20WarpRouteReader.deriveWarpRouteConfig( + warpRoute[chain].collateralVault.address, + ); + + assert( + derivedConfig.type === TokenType.collateralVault, + 'Must be collateralVault', + ); + expect(derivedConfig.type).to.equal(config[chain].type); + expect(derivedConfig.mailbox).to.equal(config[chain].mailbox); + expect(derivedConfig.owner).to.equal(config[chain].owner); + expect(derivedConfig.token).to.equal(token.address); + }); + + it('should derive rebase collateral vault config correctly', async () => { + // Create config + const config: WarpRouteDeployConfig = { + [chain]: { + type: TokenType.collateralVaultRebase, + token: vault.address, + ...baseConfig, + }, + }; + // Deploy with config + const warpRoute = await deployer.deploy(config); + // Derive config and check if each value matches + const derivedConfig = await evmERC20WarpRouteReader.deriveWarpRouteConfig( + warpRoute[chain].collateralVaultRebase.address, + ); + + assert( + derivedConfig.type === TokenType.collateralVaultRebase, + 'Must be collateralVaultRebase', + ); + expect(derivedConfig.type).to.equal(config[chain].type); + expect(derivedConfig.mailbox).to.equal(config[chain].mailbox); + expect(derivedConfig.owner).to.equal(config[chain].owner); + expect(derivedConfig.token).to.equal(token.address); + }); + it('should return undefined if ism is not set onchain', async () => { // Create config - const config = { + const config: WarpRouteDeployConfig = { [chain]: { type: TokenType.collateral, token: token.address, @@ -246,7 +334,7 @@ describe('ERC20WarpRouterReader', async () => { // Create config const otherChain = TestChainName.test3; const otherChainMetadata = test3; - const config = { + const config: WarpRouteDeployConfig = { [chain]: { type: TokenType.collateral, token: token.address, diff --git a/typescript/sdk/src/token/EvmERC20WarpRouteReader.ts b/typescript/sdk/src/token/EvmERC20WarpRouteReader.ts index 2eb3838d5..8ffbe82cc 100644 --- a/typescript/sdk/src/token/EvmERC20WarpRouteReader.ts +++ b/typescript/sdk/src/token/EvmERC20WarpRouteReader.ts @@ -4,6 +4,8 @@ import { HypERC20Collateral__factory, HypERC20__factory, HypERC4626Collateral__factory, + HypERC4626OwnerCollateral__factory, + HypERC4626__factory, TokenRouter__factory, } from '@hyperlane-xyz/core'; import { @@ -81,15 +83,23 @@ export class EvmERC20WarpRouteReader extends HyperlaneReader { const contractTypes: Partial< Record > = { - collateralVault: { + [TokenType.collateralVaultRebase]: { factory: HypERC4626Collateral__factory, + method: 'NULL_RECIPIENT', + }, + [TokenType.collateralVault]: { + factory: HypERC4626OwnerCollateral__factory, method: 'vault', }, - collateral: { + [TokenType.collateral]: { factory: HypERC20Collateral__factory, method: 'wrappedToken', }, - synthetic: { + [TokenType.syntheticRebase]: { + factory: HypERC4626__factory, + method: 'collateralDomain', + }, + [TokenType.synthetic]: { factory: HypERC20__factory, method: 'decimals', }, @@ -106,11 +116,11 @@ export class EvmERC20WarpRouteReader extends HyperlaneReader { try { const warpRoute = factory.connect(warpRouteAddress, this.provider); await warpRoute[method](); - - this.setSmartProviderLogLevel(getLogLevel()); // returns to original level defined by rootLogger return tokenType as TokenType; } catch (e) { continue; + } finally { + this.setSmartProviderLogLevel(getLogLevel()); // returns to original level defined by rootLogger } } @@ -186,7 +196,10 @@ export class EvmERC20WarpRouteReader extends HyperlaneReader { await this.fetchERC20Metadata(token); return { name, symbol, decimals, totalSupply, token }; - } else if (type === TokenType.synthetic) { + } else if ( + type === TokenType.synthetic || + type === TokenType.syntheticRebase + ) { return this.fetchERC20Metadata(tokenAddress); } else if (type === TokenType.native) { const chainMetadata = this.multiProvider.getChainMetadata(this.chain); diff --git a/typescript/sdk/src/token/Token.test.ts b/typescript/sdk/src/token/Token.test.ts index cd232ebf7..1a6ac978a 100644 --- a/typescript/sdk/src/token/Token.test.ts +++ b/typescript/sdk/src/token/Token.test.ts @@ -48,6 +48,15 @@ const STANDARD_TO_TOKEN: Record = { symbol: 'USDC', name: 'USDC', }, + [TokenStandard.EvmHypRebaseCollateral]: { + chainName: TestChainName.test3, + standard: TokenStandard.EvmHypRebaseCollateral, + addressOrDenom: '0x31b5234A896FbC4b3e2F7237592D054716762131', + collateralAddressOrDenom: '0x64544969ed7ebf5f083679233325356ebe738930', + decimals: 18, + symbol: 'USDC', + name: 'USDC', + }, [TokenStandard.EvmHypOwnerCollateral]: { chainName: TestChainName.test3, standard: TokenStandard.EvmHypOwnerCollateral, @@ -74,6 +83,14 @@ const STANDARD_TO_TOKEN: Record = { symbol: 'USDC', name: 'USDC', }, + [TokenStandard.EvmHypSyntheticRebase]: { + chainName: TestChainName.test2, + standard: TokenStandard.EvmHypSyntheticRebase, + addressOrDenom: '0x8358D8291e3bEDb04804975eEa0fe9fe0fAfB147', + decimals: 6, + symbol: 'USDC', + name: 'USDC', + }, [TokenStandard.EvmHypXERC20]: { chainName: TestChainName.test2, standard: TokenStandard.EvmHypXERC20, diff --git a/typescript/sdk/src/token/Token.ts b/typescript/sdk/src/token/Token.ts index e2541db6b..527adc2b5 100644 --- a/typescript/sdk/src/token/Token.ts +++ b/typescript/sdk/src/token/Token.ts @@ -189,19 +189,19 @@ export class Token implements IToken { return new EvmHypNativeAdapter(chainName, multiProvider, { token: addressOrDenom, }); - } else if (standard === TokenStandard.EvmHypCollateral) { - return new EvmHypCollateralAdapter(chainName, multiProvider, { - token: addressOrDenom, - }); - } else if (standard === TokenStandard.EvmHypCollateralFiat) { - return new EvmHypCollateralAdapter(chainName, multiProvider, { - token: addressOrDenom, - }); - } else if (standard === TokenStandard.EvmHypOwnerCollateral) { + } else if ( + standard === TokenStandard.EvmHypCollateral || + standard === TokenStandard.EvmHypCollateralFiat || + standard === TokenStandard.EvmHypOwnerCollateral || + standard === TokenStandard.EvmHypRebaseCollateral + ) { return new EvmHypCollateralAdapter(chainName, multiProvider, { token: addressOrDenom, }); - } else if (standard === TokenStandard.EvmHypSynthetic) { + } else if ( + standard === TokenStandard.EvmHypSynthetic || + standard === TokenStandard.EvmHypSyntheticRebase + ) { return new EvmHypSyntheticAdapter(chainName, multiProvider, { token: addressOrDenom, }); diff --git a/typescript/sdk/src/token/TokenStandard.ts b/typescript/sdk/src/token/TokenStandard.ts index 002501d32..ee434b777 100644 --- a/typescript/sdk/src/token/TokenStandard.ts +++ b/typescript/sdk/src/token/TokenStandard.ts @@ -15,8 +15,10 @@ export enum TokenStandard { EvmHypNative = 'EvmHypNative', EvmHypCollateral = 'EvmHypCollateral', EvmHypOwnerCollateral = 'EvmHypOwnerCollateral', + EvmHypRebaseCollateral = 'EvmHypRebaseCollateral', EvmHypCollateralFiat = 'EvmHypCollateralFiat', EvmHypSynthetic = 'EvmHypSynthetic', + EvmHypSyntheticRebase = 'EvmHypSyntheticRebase', EvmHypXERC20 = 'EvmHypXERC20', EvmHypXERC20Lockbox = 'EvmHypXERC20Lockbox', @@ -52,8 +54,10 @@ export const TOKEN_STANDARD_TO_PROTOCOL: Record = { EvmHypNative: ProtocolType.Ethereum, EvmHypCollateral: ProtocolType.Ethereum, EvmHypOwnerCollateral: ProtocolType.Ethereum, + EvmHypRebaseCollateral: ProtocolType.Ethereum, EvmHypCollateralFiat: ProtocolType.Ethereum, EvmHypSynthetic: ProtocolType.Ethereum, + EvmHypSyntheticRebase: ProtocolType.Ethereum, EvmHypXERC20: ProtocolType.Ethereum, EvmHypXERC20Lockbox: ProtocolType.Ethereum, @@ -114,7 +118,9 @@ export const TOKEN_HYP_STANDARDS = [ TokenStandard.EvmHypCollateral, TokenStandard.EvmHypCollateralFiat, TokenStandard.EvmHypOwnerCollateral, + TokenStandard.EvmHypRebaseCollateral, TokenStandard.EvmHypSynthetic, + TokenStandard.EvmHypSyntheticRebase, TokenStandard.EvmHypXERC20, TokenStandard.EvmHypXERC20Lockbox, TokenStandard.SealevelHypNative, @@ -148,9 +154,11 @@ export const TOKEN_TYPE_TO_STANDARD: Record = { [TokenType.XERC20]: TokenStandard.EvmHypXERC20, [TokenType.XERC20Lockbox]: TokenStandard.EvmHypXERC20Lockbox, [TokenType.collateralVault]: TokenStandard.EvmHypOwnerCollateral, + [TokenType.collateralVaultRebase]: TokenStandard.EvmHypRebaseCollateral, [TokenType.collateralUri]: TokenStandard.EvmHypCollateral, [TokenType.fastCollateral]: TokenStandard.EvmHypCollateral, [TokenType.synthetic]: TokenStandard.EvmHypSynthetic, + [TokenType.syntheticRebase]: TokenStandard.EvmHypSyntheticRebase, [TokenType.syntheticUri]: TokenStandard.EvmHypSynthetic, [TokenType.fastSynthetic]: TokenStandard.EvmHypSynthetic, [TokenType.nativeScaled]: TokenStandard.EvmHypNative, diff --git a/typescript/sdk/src/token/config.ts b/typescript/sdk/src/token/config.ts index a89264ee6..08fb750f2 100644 --- a/typescript/sdk/src/token/config.ts +++ b/typescript/sdk/src/token/config.ts @@ -1,9 +1,11 @@ export enum TokenType { synthetic = 'synthetic', + syntheticRebase = 'syntheticRebase', fastSynthetic = 'fastSynthetic', syntheticUri = 'syntheticUri', collateral = 'collateral', collateralVault = 'collateralVault', + collateralVaultRebase = 'collateralVaultRebase', XERC20 = 'xERC20', XERC20Lockbox = 'xERC20Lockbox', collateralFiat = 'collateralFiat', @@ -16,6 +18,7 @@ export enum TokenType { export const CollateralExtensions = [ TokenType.collateral, TokenType.collateralVault, + TokenType.collateralVaultRebase, ]; export const gasOverhead = (tokenType: TokenType): number => { diff --git a/typescript/sdk/src/token/contracts.ts b/typescript/sdk/src/token/contracts.ts index 281c084ff..a9ced3cb1 100644 --- a/typescript/sdk/src/token/contracts.ts +++ b/typescript/sdk/src/token/contracts.ts @@ -8,6 +8,8 @@ import { HypERC721URIStorage__factory, HypERC721__factory, HypERC4626Collateral__factory, + HypERC4626OwnerCollateral__factory, + HypERC4626__factory, HypFiatToken__factory, HypNativeScaled__factory, HypNative__factory, @@ -21,11 +23,13 @@ export const hypERC20contracts = { [TokenType.fastCollateral]: 'FastHypERC20Collateral', [TokenType.fastSynthetic]: 'FastHypERC20', [TokenType.synthetic]: 'HypERC20', + [TokenType.syntheticRebase]: 'HypERC4626', [TokenType.collateral]: 'HypERC20Collateral', [TokenType.collateralFiat]: 'HypFiatToken', [TokenType.XERC20]: 'HypXERC20', [TokenType.XERC20Lockbox]: 'HypXERC20Lockbox', - [TokenType.collateralVault]: 'HypERC20CollateralVaultDeposit', + [TokenType.collateralVault]: 'HypERC4626OwnerCollateral', + [TokenType.collateralVaultRebase]: 'HypERC4626Collateral', [TokenType.native]: 'HypNative', [TokenType.nativeScaled]: 'HypNativeScaled', }; @@ -36,7 +40,9 @@ export const hypERC20factories = { [TokenType.fastSynthetic]: new FastHypERC20__factory(), [TokenType.synthetic]: new HypERC20__factory(), [TokenType.collateral]: new HypERC20Collateral__factory(), - [TokenType.collateralVault]: new HypERC4626Collateral__factory(), + [TokenType.collateralVault]: new HypERC4626OwnerCollateral__factory(), + [TokenType.collateralVaultRebase]: new HypERC4626Collateral__factory(), + [TokenType.syntheticRebase]: new HypERC4626__factory(), [TokenType.collateralFiat]: new HypFiatToken__factory(), [TokenType.XERC20]: new HypXERC20__factory(), [TokenType.XERC20Lockbox]: new HypXERC20Lockbox__factory(), diff --git a/typescript/sdk/src/token/deploy.ts b/typescript/sdk/src/token/deploy.ts index 843f933f1..ee2a9a399 100644 --- a/typescript/sdk/src/token/deploy.ts +++ b/typescript/sdk/src/token/deploy.ts @@ -33,6 +33,7 @@ import { isCollateralConfig, isNativeConfig, isSyntheticConfig, + isSyntheticRebaseConfig, isTokenMetadata, } from './schemas.js'; import { TokenMetadata, WarpRouteDeployConfig } from './types.js'; @@ -64,6 +65,11 @@ abstract class TokenDeployer< } else if (isSyntheticConfig(config)) { assert(config.decimals, 'decimals is undefined for config'); // decimals must be defined by this point return [config.decimals, config.mailbox]; + } else if (isSyntheticRebaseConfig(config)) { + const collateralDomain = this.multiProvider.getDomainId( + config.collateralChainName, + ); + return [config.decimals, config.mailbox, collateralDomain]; } else { throw new Error('Unknown token type when constructing arguments'); } @@ -82,7 +88,7 @@ abstract class TokenDeployer< ]; if (isCollateralConfig(config) || isNativeConfig(config)) { return defaultArgs; - } else if (isSyntheticConfig(config)) { + } else if (isSyntheticConfig(config) || isSyntheticRebaseConfig(config)) { return [config.totalSupply, config.name, config.symbol, ...defaultArgs]; } else { throw new Error('Unknown collateral type when initializing arguments'); diff --git a/typescript/sdk/src/token/schemas.test.ts b/typescript/sdk/src/token/schemas.test.ts index 65f780cd0..1a8d606ea 100644 --- a/typescript/sdk/src/token/schemas.test.ts +++ b/typescript/sdk/src/token/schemas.test.ts @@ -1,8 +1,15 @@ import { expect } from 'chai'; import { ethers } from 'ethers'; +import { assert } from '@hyperlane-xyz/utils'; + import { TokenType } from './config.js'; -import { WarpRouteDeployConfigSchema } from './schemas.js'; +import { + WarpRouteDeployConfigSchema, + WarpRouteDeployConfigSchemaErrors, + isCollateralConfig, +} from './schemas.js'; +import { WarpRouteDeployConfig } from './types.js'; const SOME_ADDRESS = ethers.Wallet.createRandom().address; const COLLATERAL_TYPES = [ @@ -19,7 +26,7 @@ const NON_COLLATERAL_TYPES = [ ]; describe('WarpRouteDeployConfigSchema refine', () => { - let config: any; + let config: WarpRouteDeployConfig; beforeEach(() => { config = { arbitrum: { @@ -33,18 +40,24 @@ describe('WarpRouteDeployConfigSchema refine', () => { it('should require token type', () => { expect(WarpRouteDeployConfigSchema.safeParse(config).success).to.be.true; + + //@ts-ignore delete config.arbitrum.type; expect(WarpRouteDeployConfigSchema.safeParse(config).success).to.be.false; }); it('should require token address', () => { expect(WarpRouteDeployConfigSchema.safeParse(config).success).to.be.true; + + //@ts-ignore delete config.arbitrum.token; expect(WarpRouteDeployConfigSchema.safeParse(config).success).to.be.false; }); it('should require mailbox address', () => { expect(WarpRouteDeployConfigSchema.safeParse(config).success).to.be.true; + + //@ts-ignore delete config.arbitrum.mailbox; expect(WarpRouteDeployConfigSchema.safeParse(config).success).to.be.false; }); @@ -52,6 +65,9 @@ describe('WarpRouteDeployConfigSchema refine', () => { it('should throw if collateral type and token is empty', async () => { for (const type of COLLATERAL_TYPES) { config.arbitrum.type = type; + assert(isCollateralConfig(config.arbitrum), 'must be collateral'); + + //@ts-ignore config.arbitrum.token = undefined; expect(WarpRouteDeployConfigSchema.safeParse(config).success).to.be.false; @@ -61,13 +77,8 @@ describe('WarpRouteDeployConfigSchema refine', () => { } }); - it('should accept native type if token is empty', async () => { - config.arbitrum.type = TokenType.native; - config.arbitrum.token = undefined; - expect(WarpRouteDeployConfigSchema.safeParse(config).success).to.be.true; - }); - it('should succeed if non-collateral type, token is empty, metadata is defined', async () => { + //@ts-ignore delete config.arbitrum.token; config.arbitrum.totalSupply = '0'; config.arbitrum.name = 'name'; @@ -81,4 +92,93 @@ describe('WarpRouteDeployConfigSchema refine', () => { expect(WarpRouteDeployConfigSchema.safeParse(config).success).to.be.true; } }); + + it(`should throw if deploying rebasing collateral with anything other than ${TokenType.syntheticRebase}`, async () => { + config = { + arbitrum: { + type: TokenType.collateralVaultRebase, + token: SOME_ADDRESS, + owner: SOME_ADDRESS, + mailbox: SOME_ADDRESS, + }, + ethereum: { + type: TokenType.collateralVault, + token: SOME_ADDRESS, + owner: SOME_ADDRESS, + mailbox: SOME_ADDRESS, + }, + optimism: { + type: TokenType.syntheticRebase, + owner: SOME_ADDRESS, + mailbox: SOME_ADDRESS, + collateralChainName: '', + }, + }; + let parseResults = WarpRouteDeployConfigSchema.safeParse(config); + assert(!parseResults.success, 'must be false'); // Needed so 'message' shows up because parseResults is a discriminate union + expect(parseResults.error.issues[0].message).to.equal( + WarpRouteDeployConfigSchemaErrors.ONLY_SYNTHETIC_REBASE, + ); + + config.ethereum.type = TokenType.syntheticRebase; + //@ts-ignore + config.ethereum.collateralChainName = ''; + parseResults = WarpRouteDeployConfigSchema.safeParse(config); + //@ts-ignore + expect(parseResults.success).to.be.true; + }); + + it(`should throw if deploying only ${TokenType.collateralVaultRebase}`, async () => { + config = { + arbitrum: { + type: TokenType.collateralVaultRebase, + token: SOME_ADDRESS, + owner: SOME_ADDRESS, + mailbox: SOME_ADDRESS, + }, + }; + let parseResults = WarpRouteDeployConfigSchema.safeParse(config); + expect(parseResults.success).to.be.false; + + config.ethereum = { + type: TokenType.collateralVaultRebase, + token: SOME_ADDRESS, + owner: SOME_ADDRESS, + mailbox: SOME_ADDRESS, + }; + parseResults = WarpRouteDeployConfigSchema.safeParse(config); + expect(parseResults.success).to.be.false; + }); + + it(`should derive the collateral chain name for ${TokenType.syntheticRebase}`, async () => { + config = { + arbitrum: { + type: TokenType.collateralVaultRebase, + token: SOME_ADDRESS, + owner: SOME_ADDRESS, + mailbox: SOME_ADDRESS, + }, + ethereum: { + type: TokenType.syntheticRebase, + owner: SOME_ADDRESS, + mailbox: SOME_ADDRESS, + collateralChainName: '', + }, + optimism: { + type: TokenType.syntheticRebase, + owner: SOME_ADDRESS, + mailbox: SOME_ADDRESS, + collateralChainName: '', + }, + }; + const parseResults = WarpRouteDeployConfigSchema.safeParse(config); + assert(parseResults.success, 'must be true'); + const warpConfig: WarpRouteDeployConfig = parseResults.data; + + assert( + warpConfig.optimism.type === TokenType.syntheticRebase, + 'must be syntheticRebase', + ); + expect(warpConfig.optimism.collateralChainName).to.equal('arbitrum'); + }); }); diff --git a/typescript/sdk/src/token/schemas.ts b/typescript/sdk/src/token/schemas.ts index 8ce070c1e..1ef4a770b 100644 --- a/typescript/sdk/src/token/schemas.ts +++ b/typescript/sdk/src/token/schemas.ts @@ -1,10 +1,16 @@ import { z } from 'zod'; +import { objMap } from '@hyperlane-xyz/utils'; + import { GasRouterConfigSchema } from '../router/schemas.js'; import { isCompliant } from '../utils/schemas.js'; import { TokenType } from './config.js'; +export const WarpRouteDeployConfigSchemaErrors = { + ONLY_SYNTHETIC_REBASE: `Config with ${TokenType.collateralVaultRebase} must be deployed with ${TokenType.syntheticRebase}`, + NO_SYNTHETIC_ONLY: `Config must include Native or Collateral OR all synthetics must define token metadata`, +}; export const TokenMetadataSchema = z.object({ name: z.string(), symbol: z.string(), @@ -18,6 +24,7 @@ export const CollateralConfigSchema = TokenMetadataSchema.partial().extend({ type: z.enum([ TokenType.collateral, TokenType.collateralVault, + TokenType.collateralVaultRebase, TokenType.XERC20, TokenType.XERC20Lockbox, TokenType.collateralFiat, @@ -33,6 +40,18 @@ export const NativeConfigSchema = TokenMetadataSchema.partial().extend({ type: z.enum([TokenType.native, TokenType.nativeScaled]), }); +export const CollateralRebaseConfigSchema = + TokenMetadataSchema.partial().extend({ + type: z.literal(TokenType.collateralVaultRebase), + }); + +export const SyntheticRebaseConfigSchema = TokenMetadataSchema.partial().extend( + { + type: z.literal(TokenType.syntheticRebase), + collateralChainName: z.string(), + }, +); + export const SyntheticConfigSchema = TokenMetadataSchema.partial().extend({ type: z.enum([ TokenType.synthetic, @@ -50,6 +69,7 @@ export const TokenConfigSchema = z.discriminatedUnion('type', [ NativeConfigSchema, CollateralConfigSchema, SyntheticConfigSchema, + SyntheticRebaseConfigSchema, ]); export const TokenRouterConfigSchema = TokenConfigSchema.and( @@ -61,6 +81,10 @@ export type NativeConfig = z.infer; export type CollateralConfig = z.infer; export const isSyntheticConfig = isCompliant(SyntheticConfigSchema); +export const isSyntheticRebaseConfig = isCompliant(SyntheticRebaseConfigSchema); +export const isCollateralRebaseConfig = isCompliant( + CollateralRebaseConfigSchema, +); export const isCollateralConfig = isCompliant(CollateralConfigSchema); export const isNativeConfig = isCompliant(NativeConfigSchema); export const isTokenMetadata = isCompliant(TokenMetadataSchema); @@ -71,7 +95,49 @@ export const WarpRouteDeployConfigSchema = z const entries = Object.entries(configMap); return ( entries.some( - ([_, config]) => isCollateralConfig(config) || isNativeConfig(config), + ([_, config]) => + isCollateralConfig(config) || + isCollateralRebaseConfig(config) || + isNativeConfig(config), ) || entries.every(([_, config]) => isTokenMetadata(config)) ); - }, `Config must include Native or Collateral OR all synthetics must define token metadata`); + }, WarpRouteDeployConfigSchemaErrors.NO_SYNTHETIC_ONLY) + .transform((warpRouteDeployConfig, ctx) => { + const collateralRebaseEntry = Object.entries(warpRouteDeployConfig).find( + ([_, config]) => isCollateralRebaseConfig(config), + ); + if (!collateralRebaseEntry) return warpRouteDeployConfig; // Pass through for other token types + + if (isCollateralRebasePairedCorrectly(warpRouteDeployConfig)) { + const collateralChainName = collateralRebaseEntry[0]; + return objMap(warpRouteDeployConfig, (_, config) => { + if (config.type === TokenType.syntheticRebase) + config.collateralChainName = collateralChainName; + return config; + }) as Record; + } + + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: WarpRouteDeployConfigSchemaErrors.ONLY_SYNTHETIC_REBASE, + }); + + return z.NEVER; // Causes schema validation to throw with above issue + }); + +function isCollateralRebasePairedCorrectly( + warpRouteDeployConfig: Record, +): boolean { + // Filter out all the non-collateral rebase configs to check if they are only synthetic rebase tokens + const otherConfigs = Object.entries(warpRouteDeployConfig).filter( + ([_, config]) => !isCollateralRebaseConfig(config), + ); + + if (otherConfigs.length === 0) return false; + + // The other configs MUST be synthetic rebase + const allOthersSynthetic: boolean = otherConfigs.every( + ([_, config], _index) => isSyntheticRebaseConfig(config), + ); + return allOthersSynthetic; +} diff --git a/yarn.lock b/yarn.lock index bae817923..910c021e8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7827,6 +7827,7 @@ __metadata: "@hyperlane-xyz/sdk": "npm:5.5.0" "@hyperlane-xyz/utils": "npm:5.5.0" "@inquirer/prompts": "npm:^3.0.0" + "@types/chai-as-promised": "npm:^8" "@types/mocha": "npm:^10.0.1" "@types/node": "npm:^18.14.5" "@types/yargs": "npm:^17.0.24" @@ -7834,7 +7835,8 @@ __metadata: "@typescript-eslint/parser": "npm:^7.4.0" asn1.js: "npm:^5.4.1" bignumber.js: "npm:^9.1.1" - chai: "npm:4.5.0" + chai: "npm:^4.5.0" + chai-as-promised: "npm:^8.0.0" chalk: "npm:^5.3.0" eslint: "npm:^8.57.0" eslint-config-prettier: "npm:^9.1.0" @@ -13119,6 +13121,15 @@ __metadata: languageName: node linkType: hard +"@types/chai-as-promised@npm:^8": + version: 8.0.0 + resolution: "@types/chai-as-promised@npm:8.0.0" + dependencies: + "@types/chai": "npm:*" + checksum: f6db5698e4f28fd6e3914740810f356269b7f4e93a0650b38a9b01a1bae030593487c80bc57a0e69dd0bfb069a61d3dd285bfcfba6d1daf66ef3939577b68169 + languageName: node + linkType: hard + "@types/chai@npm:*, @types/chai@npm:^4.2.21": version: 4.3.1 resolution: "@types/chai@npm:4.3.1" @@ -16218,7 +16229,18 @@ __metadata: languageName: node linkType: hard -"chai@npm:4.5.0, chai@npm:^4.3.10, chai@npm:^4.3.7": +"chai-as-promised@npm:^8.0.0": + version: 8.0.0 + resolution: "chai-as-promised@npm:8.0.0" + dependencies: + check-error: "npm:^2.0.0" + peerDependencies: + chai: ">= 2.1.2 < 6" + checksum: 91d6a49caac7965440b8f8af421ebe6f060a3b5523599ae143816d08fc19d9a971ea2bc5401f82ce88d15d8bc7b64d356bf3e53542ace9e2f25cc454164d3247 + languageName: node + linkType: hard + +"chai@npm:4.5.0, chai@npm:^4.3.10, chai@npm:^4.3.7, chai@npm:^4.5.0": version: 4.5.0 resolution: "chai@npm:4.5.0" dependencies: @@ -16338,6 +16360,13 @@ __metadata: languageName: node linkType: hard +"check-error@npm:^2.0.0": + version: 2.1.1 + resolution: "check-error@npm:2.1.1" + checksum: d785ed17b1d4a4796b6e75c765a9a290098cf52ff9728ce0756e8ffd4293d2e419dd30c67200aee34202463b474306913f2fcfaf1890641026d9fc6966fea27a + languageName: node + linkType: hard + "chokidar@npm:3.3.0": version: 3.3.0 resolution: "chokidar@npm:3.3.0"