Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
205 changes: 126 additions & 79 deletions tasks/enableL1TokenAcrossEcosystem.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,39 @@
import { task } from "hardhat/config";
import assert from "assert";
import { findL2TokenForL1Token, askYesNoQuestion, zeroAddress, minimalSpokePoolInterface } from "./utils";
import { askYesNoQuestion, minimalSpokePoolInterface } from "./utils";
import { TOKEN_SYMBOLS_MAP } from "../utils/constants";

const enabledChainIds = [1, 10, 137, 42161]; // Supported mainnet chain IDs.
const enabledChainIds = [1, 10, 137, 42161, 324]; // Supported mainnet chain IDs.

const getChainsFromList = (taskArgInput: string): number[] =>
taskArgInput
?.replace(/\s/g, "")
?.split(",")
?.map((chainId: string) => Number(chainId)) || [];

task("enable-l1-token-across-ecosystem", "Enable a provided token across the entire ecosystem of supported chains")
.addParam("chain1token", "Address of the token to enable, as defined on L1")
.addFlag("execute", "Provide this flag if you would like to actually execute the transaction from the EOA")
.addOptionalParam("chain10token", "Address of the token on chainID 10. Used to override the auto detect")
.addOptionalParam("chain137token", "Address of the token on chainID 137. Used to override the auto detect")
.addOptionalParam("chain42161token", "Address of the token on chainID 42161. Used to override the auto detect")
.addParam("token", "Symbol of token to enable")
.addParam("chains", "Comma-delimited list of chains to enable the token on. Defaults to all supported chains")
.addOptionalParam(
"customoptimismbridge",
"Custom token bridge to set for optimism, for example used with SNX and DAI"
)
.addOptionalParam("ignorechains", "ChainIds to ignore. Separated by comma.")
.addOptionalParam("depositroutechains", "ChainIds to enable deposit routes for exclusively. Separated by comma.")
.setAction(async function (taskArguments, hre_) {
const hre = hre_ as any;
const l1Token = taskArguments.chain1token;
assert(l1Token, "chain1token argument must be provided");
const ignoredChainIds: number[] =
taskArguments.ignorechains
?.replace(/\s/g, "")
?.split(",")
?.map((chainId: string) => Number(chainId)) || [];
if (ignoredChainIds.includes(1)) throw new Error("Cannot ignore chainId 1");
const matchedSymbol = Object.keys(TOKEN_SYMBOLS_MAP).find(
(symbol) => symbol === taskArguments.token
) as keyof typeof TOKEN_SYMBOLS_MAP;
assert(matchedSymbol !== undefined, `Could not find token with symbol ${taskArguments.token} in TOKEN_SYMBOLS_MAP`);
const l1Token = TOKEN_SYMBOLS_MAP[matchedSymbol];

// If deposit routes chains are provided then we'll only add routes involving these chains. This is used to add new
// deposit routes to a new chain for an existing L1 token, so we also won't add a new LP token if this is defined.
const depositRouteChains = getChainsFromList(taskArguments.depositroutechains);
if (depositRouteChains.length > 0) {
console.log(`\n0. Only adding deposit routes involving chains on list ${depositRouteChains.join(", ")}`);
}

const hasSetConfigStore = await askYesNoQuestion(
`\nHave you setup the ConfigStore for this token? If not then this script will exit because a rate model must be set before the first deposit is sent otherwise the bots will error out`
Expand All @@ -36,42 +45,41 @@ task("enable-l1-token-across-ecosystem", "Enable a provided token across the ent
const signer = (await hre.ethers.getSigners())[0];

// Remove chainIds that are in the ignore list.
let chainIds = enabledChainIds.filter((chainId) => !ignoredChainIds.includes(chainId));

console.log("\n1. Auto detecting L2 companion token address for provided L1 token.");
const autoDetectedTokens = await Promise.all(
chainIds.slice(1).map((chainId) => findL2TokenForL1Token(chainId, l1Token))
);

let tokens: string[] = [];
tokens[0] = l1Token;
chainIds
.slice(1)
.forEach(
(chainId, index) => (tokens[index + 1] = taskArguments[`chain${chainId}token`] ?? autoDetectedTokens[index])
let inputChains: number[] = [];
try {
const parsedChains: string[] = taskArguments.chains.split(",");
inputChains = parsedChains.map((x) => Number(x));
console.log(`\nParsed 'chains' argument:`, inputChains);
} catch (error) {
throw new Error(
`Failed to parse 'chains' argument ${taskArguments.chains} as a comma-separated list of numbers.`
);

for (let i = 0; i < chainIds.length; i++) {
const chainId = chainIds[i];
if (
tokens[i] === zeroAddress &&
!(await askYesNoQuestion(
`\nNo address found for chainId: ${chainId}. Would you like to remove routes to and from this chain?`
))
) {
console.log(`Please rerun with override address for chainId: ${chainId}`);
process.exit(0);
}
}

chainIds = chainIds.filter((_, index) => tokens[index] !== zeroAddress);
tokens = tokens.filter((token) => token !== zeroAddress);
if (inputChains.length === 0) inputChains = enabledChainIds;
else if (inputChains.some((chain) => isNaN(chain) || !Number.isInteger(chain) || chain < 0)) {
throw new Error(`Invalid chains list: ${inputChains}`);
}
const chainIds = enabledChainIds.filter((chainId) => inputChains.includes(chainId));

console.log("\n1. Loading L2 companion token address for provided L1 token.");
const tokens = await Promise.all(
chainIds.map((chainId) => {
const l2Address = TOKEN_SYMBOLS_MAP[matchedSymbol].addresses[chainId];
if (l2Address === undefined) {
throw new Error(`Could not find token address on chain ${chainId} in TOKEN_SYMBOLS_MAP`);
}
return l2Address;
})
);

console.table(
chainIds.map((chainId, index) => {
return { chainId, address: tokens[index], autoDetected: taskArguments[`chain${chainId}token`] === undefined };
return {
chainId,
address: tokens[index],
};
}),
["chainId", "address", "autoDetected"]
["chainId", "address"]
);

// Check the user is ok with the token addresses provided. If not, abort.
Expand All @@ -85,52 +93,91 @@ task("enable-l1-token-across-ecosystem", "Enable a provided token across the ent

// Construct calldata to enable these tokens.
const callData = [];
console.log("\n5. Adding calldata to enable liquidity provision on", l1Token);
callData.push(hubPool.interface.encodeFunctionData("enableL1TokenForLiquidityProvision", [l1Token]));

// If deposit route chains are defined then we don't want to add a new LP token:
if (depositRouteChains.length === 0) {
console.log("\n5. Adding calldata to enable liquidity provision on", l1Token);
callData.push(hubPool.interface.encodeFunctionData("enableL1TokenForLiquidityProvision", [l1Token]));
}

console.log("\n6. Adding calldata to enable routes between all chains and tokens:");
let i = 0; // counter for logging.
chainIds.forEach((fromId, fromIndex) => {
chainIds.forEach((toId, _) => {
if (fromId === toId) return;

console.log(`\t 6.${++i}\t Adding calldata for token ${tokens[fromIndex]} for route ${fromId} -> ${toId}`);
callData.push(hubPool.interface.encodeFunctionData("setDepositRoute", [fromId, toId, tokens[fromIndex], true]));
// If deposit route chains are defined, only add route if it involves a chain on that list
if (
depositRouteChains.length === 0 ||
depositRouteChains.includes(toId) ||
depositRouteChains.includes(fromId)
) {
console.log(`\t 6.${++i}\t Adding calldata for token ${tokens[fromIndex]} for route ${fromId} -> ${toId}`);
callData.push(
hubPool.interface.encodeFunctionData("setDepositRoute", [fromId, toId, tokens[fromIndex], true])
);
} else {
console.log(
`\t\t Skipping route ${fromId} -> ${toId} because it doesn't involve a chain on the exclusive list`
);
}
});
});

console.log("\n7. Adding calldata to set the pool rebalance route for the respective destination tokens:");
chainIds.forEach((toId, toIndex) => {
console.log(`\t 7.${toIndex}\t Adding calldata for rebalance route for L2Token ${tokens[toIndex]} on ${toId}`);
callData.push(hubPool.interface.encodeFunctionData("setPoolRebalanceRoute", [toId, l1Token, tokens[toIndex]]));
});
// If deposit route chains are defined then we don't want to add a new PoolRebalanceRoute
if (depositRouteChains.length === 0) {
console.log("\n7. Adding calldata to set the pool rebalance route for the respective destination tokens:");
let j = 0; // counter for logging.
chainIds.forEach((toId, toIndex) => {
// If deposit route chains are defined, only add route if it involves a chain on that list
if (depositRouteChains.length === 0 || depositRouteChains.includes(toId)) {
console.log(`\t 7.${++j}\t Adding calldata for rebalance route for L2Token ${tokens[toIndex]} on ${toId}`);
callData.push(
hubPool.interface.encodeFunctionData("setPoolRebalanceRoute", [toId, l1Token, tokens[toIndex]])
);
} else {
console.log(
`\t\t Skipping pool rebalance rout -> ${toId} because it doesn't involve a chain on the exclusive list`
);
}
});

if (chainIds.includes(42161)) {
console.log("\n8. Adding call data to whitelist L1 token on Arbitrum. This is only needed on this chain");

// Address doesn't matter, we only want the interface.
const spokePool = new ethers.Contract(hubPoolDeployment.address, minimalSpokePoolInterface, signer);
// Find the address of the the Arbitrum representation of this token. Construct whitelistToken call to send to the
// Arbitrum spoke pool via the relaySpokeAdminFunction call.
const arbitrumToken = tokens[chainIds.indexOf(42161)];
const whitelistTokenCallData = spokePool.interface.encodeFunctionData("whitelistToken", [arbitrumToken, l1Token]);
callData.push(
hubPool.interface.encodeFunctionData("relaySpokePoolAdminFunction", [42161, whitelistTokenCallData])
);
}
// We only need to whitelist an Arbitrum token on the SpokePool if we're setting up a pool rebalance route between
// mainnet and Arbitrum, so if deposit route chains are set then no need to do this.
if (chainIds.includes(42161)) {
const arbitrumToken = tokens[chainIds.indexOf(42161)];
console.log(
`\n8. Adding call data to whitelist L2 ${arbitrumToken} -> L1 token ${l1Token} on Arbitrum. This is only needed on this chain`
);

// Address doesn't matter, we only want the interface.
const spokePool = new ethers.Contract(hubPoolDeployment.address, minimalSpokePoolInterface, signer);
// Find the address of the the Arbitrum representation of this token. Construct whitelistToken call to send to the
// Arbitrum spoke pool via the relaySpokeAdminFunction call.
const whitelistTokenCallData = spokePool.interface.encodeFunctionData("whitelistToken", [
arbitrumToken,
l1Token,
]);
callData.push(
hubPool.interface.encodeFunctionData("relaySpokePoolAdminFunction", [42161, whitelistTokenCallData])
);
}

// Add optimism setTokenBridge call
if (chainIds.includes(10) && taskArguments.customoptimismbridge) {
console.log("\n9. Adding call data to set custom Optimism bridge.");

// Address doesn't matter, we only want the interface:
const spokePool = new ethers.Contract(hubPoolDeployment.address, minimalSpokePoolInterface, signer);
const optimismToken = tokens[chainIds.indexOf(10)];
const setTokenBridgeCallData = spokePool.interface.encodeFunctionData("setTokenBridge", [
optimismToken,
taskArguments.customoptimismbridge,
]);
callData.push(hubPool.interface.encodeFunctionData("relaySpokePoolAdminFunction", [10, setTokenBridgeCallData]));
// Add optimism setTokenBridge call if the token has a custom bridge needed to get to mainnet.
if (chainIds.includes(10) && taskArguments.customoptimismbridge) {
console.log("\n9. Adding call data to set custom Optimism bridge.");

// Address doesn't matter, we only want the interface:
const spokePool = new ethers.Contract(hubPoolDeployment.address, minimalSpokePoolInterface, signer);
const optimismToken = tokens[chainIds.indexOf(10)];
const setTokenBridgeCallData = spokePool.interface.encodeFunctionData("setTokenBridge", [
optimismToken,
taskArguments.customoptimismbridge,
]);
callData.push(
hubPool.interface.encodeFunctionData("relaySpokePoolAdminFunction", [10, setTokenBridgeCallData])
);
}
}

console.log(`\n10. ***DONE.***\nCalldata to enable desired token has been constructed!`);
Expand Down
109 changes: 0 additions & 109 deletions tasks/utils.ts
Original file line number Diff line number Diff line change
@@ -1,110 +1,7 @@
import fetch from "node-fetch";
import { ethers } from "ethers";
import readline from "readline";
export const zeroAddress = ethers.constants.AddressZero;

export async function findL2TokenForL1Token(l2ChainId: number, l1TokenAddress: string) {
if (l2ChainId === 10) {
const foundOnChain = await _findL2TokenForOvmChain(l2ChainId, l1TokenAddress);
if (foundOnChain !== zeroAddress) return foundOnChain;
else return await _findL2TokenFromTokenList(l2ChainId, l1TokenAddress);
}
if (l2ChainId === 137) return await _findL2TokenFromTokenList(l2ChainId, l1TokenAddress);
if (l2ChainId === 288) {
const foundOnChain = await _findL2TokenForOvmChain(l2ChainId, l1TokenAddress);
if (foundOnChain !== zeroAddress) return foundOnChain;
else return await _findL2TokenFromTokenList(l2ChainId, l1TokenAddress);
}
if (l2ChainId === 42161) return await _findL2TokenFromTokenList(l2ChainId, l1TokenAddress);
}

async function _findL2TokenFromTokenList(l2ChainId: number, l1TokenAddress: string) {
if (l2ChainId === 10) {
const response = await fetch("https://static.optimism.io/optimism.tokenlist.json");
const body = await response.text();
const tokenList = JSON.parse(body).tokens;
const searchSymbol = tokenList.find(
(element: any) => element.chainId === 1 && element.address.toLowerCase() === l1TokenAddress.toLocaleLowerCase()
)?.symbol;
if (!searchSymbol) return zeroAddress;
return tokenList.find((element: any) => element.chainId === 10 && element.symbol === searchSymbol).address;
}
if (l2ChainId === 137) {
const response = await fetch(
"https://raw.githubusercontent.com/maticnetwork/polygon-token-list/master/src/tokens/polygonTokens.json"
);
const body = await response.text();
const tokenList = JSON.parse(body);
const l2Address = tokenList.find(
(element: any) => element?.extensions?.rootAddress?.toLowerCase() === l1TokenAddress.toLowerCase()
)?.address;
return l2Address ?? zeroAddress;
}
if (l2ChainId === 42161) {
const response = await fetch("https://bridge.arbitrum.io/token-list-42161.json");
const body = await response.text();
const tokenList = JSON.parse(body).tokens;
const l2Address = tokenList.find(
(element: any) => element?.extensions?.l1Address?.toLowerCase() === l1TokenAddress.toLowerCase()
)?.address;
return l2Address ?? zeroAddress;
} else if (l2ChainId === 288) {
const url =
"https://raw.githubusercontent.com/bobanetwork/boba/develop/packages/boba/register/addresses/addressesMainnet_0x8376ac6C3f73a25Dd994E0b0669ca7ee0C02F089.json";
const response = await fetch(url);
const body = await response.text();
const tokenList = JSON.parse(body) as { [name: string]: string };
const l1TokenName = Object.entries(tokenList).find(([, address]) => {
return address.toLowerCase() === l1TokenAddress.toLowerCase();
})?.[0];
if (!l1TokenName) return zeroAddress;
if (!l1TokenName.includes("L1")) {
console.error(
`L1 not labeled as expected. Address: ${tokenList[l1TokenName]}, name: ${l1TokenName} at url: ${url}`
);
return zeroAddress;
}
const l2TokenName = l1TokenName.replace("L1", "L2");
const l2TokenAddress = tokenList[l2TokenName];
if (!l2TokenAddress) {
console.error(`L2 token address not found. name ${l2TokenName} url: ${url}`);
return zeroAddress;
}
return l2TokenAddress;
}
return zeroAddress;
}

async function _findL2TokenForOvmChain(l2ChainId: number, l1TokenAddress: string) {
const ovmL2StandardERC20 = "0x4200000000000000000000000000000000000010";
const l2Bridge = new ethers.Contract(ovmL2StandardERC20, ovmBridgeAbi as any, createConnectedVoidSigner(l2ChainId));

const depositFinalizedEvents = (await l2Bridge.queryFilter(
l2Bridge.filters.DepositFinalized(l1TokenAddress),
-4999,
"latest"
)) as any;

if (depositFinalizedEvents.length === 0) return zeroAddress;
return depositFinalizedEvents[0].args._l2Token;
}

const ovmBridgeAbi = [
{
anonymous: false,
inputs: [
{ indexed: true, internalType: "address", name: "_l1Token", type: "address" },
{ indexed: true, internalType: "address", name: "_l2Token", type: "address" },
{ indexed: true, internalType: "address", name: "_from", type: "address" },
{ indexed: false, internalType: "address", name: "_to", type: "address" },
{ indexed: false, internalType: "uint256", name: "_amount", type: "uint256" },
{ indexed: false, internalType: "bytes", name: "_data", type: "bytes" },
],
name: "DepositFinalized",
type: "event",
},
];

export const minimalSpokePoolInterface = [
{
inputs: [
Expand All @@ -128,12 +25,6 @@ export const minimalSpokePoolInterface = [
},
];

export function createConnectedVoidSigner(networkId: number) {
const nodeUrl = process.env[`NODE_URL_${networkId}`];
if (!nodeUrl) throw new Error(`No NODE_URL_${networkId} set`);
return new ethers.VoidSigner(zeroAddress).connect(new ethers.providers.JsonRpcProvider(nodeUrl));
}

async function askQuestion(query: string) {
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });

Expand Down