Skip to content

Commit

Permalink
Script + safeTransferFromLegacy (matter-labs#402)
Browse files Browse the repository at this point in the history
  • Loading branch information
StanislavBreadless authored Apr 24, 2024
1 parent 5016910 commit fcee799
Show file tree
Hide file tree
Showing 4 changed files with 247 additions and 4 deletions.
24 changes: 22 additions & 2 deletions l1-contracts/contracts/bridge/L1SharedBridge.sol
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,12 @@ contract L1SharedBridge is IL1SharedBridge, ReentrancyGuard, Ownable2StepUpgrade
_;
}

/// @notice Checks that the message sender is the shared bridge itself.
modifier onlySelf() {
require(msg.sender == address(this), "ShB not shared bridge");
_;
}

/// @dev Contract is expected to be used as proxy implementation.
/// @dev Initialize the implementation to prevent Parity hack.
constructor(
Expand Down Expand Up @@ -161,7 +167,7 @@ contract L1SharedBridge is IL1SharedBridge, ReentrancyGuard, Ownable2StepUpgrade
}

/// @dev transfer tokens from legacy erc20 bridge or mailbox and set chainBalance as part of migration process
function transferFundsFromLegacy(address _token, address _target, uint256 _targetChainId) external onlyOwner {
function transferFundsFromLegacy(address _token, address _target, uint256 _targetChainId) external onlySelf {
if (_token == ETH_TOKEN_ADDRESS) {
uint256 balanceBefore = address(this).balance;
IMailbox(_target).transferEthToSharedBridge();
Expand All @@ -177,11 +183,25 @@ contract L1SharedBridge is IL1SharedBridge, ReentrancyGuard, Ownable2StepUpgrade
require(legacyBridgeBalance > 0, "ShB: 0 amount to transfer");
IL1ERC20Bridge(_target).transferTokenToSharedBridge(_token);
uint256 balanceAfter = IERC20(_token).balanceOf(address(this));
require(balanceAfter - balanceBefore == legacyBridgeBalance, "ShB: wrong amount transferred");
require(balanceAfter - balanceBefore >= legacyBridgeBalance, "ShB: wrong amount transferred");
chainBalance[_targetChainId][_token] = chainBalance[_targetChainId][_token] + legacyBridgeBalance;
}
}

/// @dev transfer tokens from legacy erc20 bridge or mailbox and set chainBalance as part of migration process.
/// @dev Unlike `transferFundsFromLegacy` is provides a concrete limit on the gas used for the transfer and even if it will fail, it will not revert the whole transaction.
function safeTransferFundsFromLegacy(
address _token,
address _target,
uint256 _targetChainId,
uint256 _gasPerToken
) external onlyOwner {
try this.transferFundsFromLegacy{gas: _gasPerToken}(_token, _target, _targetChainId) {} catch {
// A reasonable amount of gas will be provided to transfer the token.
// If the transfer fails, we don't want to revert the whole transaction.
}
}

function receiveEth(uint256 _chainId) external payable {
require(BRIDGE_HUB.getHyperchain(_chainId) == msg.sender, "receiveEth not state transition");
}
Expand Down
1 change: 1 addition & 0 deletions l1-contracts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
"hyperchain-upgrade-1": "ts-node scripts/hyperchain-upgrade-1.ts",
"hyperchain-upgrade-2": "ts-node scripts/hyperchain-upgrade-2.ts",
"hyperchain-upgrade-3": "ts-node scripts/hyperchain-upgrade-3.ts",
"token-migration": "ts-node scripts/token-migration.ts",
"setup-legacy-bridge-era": "ts-node scripts/setup-legacy-bridge-era.ts"
},
"dependencies": {
Expand Down
220 changes: 220 additions & 0 deletions l1-contracts/scripts/token-migration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
// hardhat import should be the first import in the file
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import * as hardhat from "hardhat";
import { Command } from "commander";
import { web3Url } from "./utils";
import { ethers } from "ethers";
import { Provider, utils } from "zksync-ethers";

async function main() {
const program = new Command();

program.version("0.1.0").name("upgrade-shared-bridge-era").description("upgrade shared bridge for era diamond proxy");

program
.command("get-confirmed-tokens")
.description("Returns the list of tokens that are registered on the bridge and should be migrated")
.option("--use-l1")
.option("--start-from-block <startFromBlock>")
.action(async (cmd) => {
const l2Provider = new Provider(process.env.API_WEB3_JSON_RPC_HTTP_URL);
const l1Provider = new ethers.providers.JsonRpcProvider(web3Url());

let confirmedFromAPI;

if (cmd.useL1) {
const block = cmd.startFromBlock;
if (!block) {
throw new Error("For L1 the starting block should be provided");
}

console.log("Fetching confirmed tokens from the L1");
console.log("This will take a long time");

const bridge = (await l2Provider.getDefaultBridgeAddresses()).erc20L1;
console.log("Using L1 ERC20 bridge ", bridge);

const confirmedFromL1 = await loadAllConfirmedTokensFromL1(l1Provider, bridge, +block);
console.log(JSON.stringify(confirmedFromL1, null, 2));
} else {
console.log("Fetching confirmed tokens from the L2 API...");
confirmedFromAPI = await loadAllConfirmedTokensFromAPI(l2Provider);

console.log(JSON.stringify(confirmedFromAPI, null, 2));
}
});

program
.command("merge-confirmed-tokens")
.description("Merges two lists of confirmed tokens")
.option("--from-l1 <tokensFromL1>")
.option("--from-l2 <tokensFromL2>")
.action(async (cmd) => {
const l2Provider = new Provider(process.env.API_WEB3_JSON_RPC_HTTP_URL);
const bridge = (await l2Provider.getDefaultBridgeAddresses()).erc20L1;
console.log("Using L1 ERC20 bridge ", bridge);

const allTokens = {};
const tokensFromL1: string[] = JSON.parse(cmd.fromL1).map((token) => token.toLowerCase());
const tokensFromL2: string[] = JSON.parse(cmd.fromL2).map((token) => token.toLowerCase());

tokensFromL1.forEach((token) => (allTokens[token] = true));
tokensFromL2.forEach((token) => (allTokens[token] = true));

const erc20Abi = ["function balanceOf(address) view returns (uint256)"];

const result = [];

const l1Provider = new ethers.providers.JsonRpcProvider(web3Url());
for (const token of Object.keys(allTokens)) {
const contract = new ethers.Contract(token, erc20Abi, l1Provider);
const balanceL1 = await contract.balanceOf(bridge);
if (balanceL1.gt(0)) {
console.log("Token ", token, " has balance in the bridge ", balanceL1.toString());
result.push(token);
}
}

console.log(JSON.stringify(result, null, 2));
});

program
.command("prepare-migration-calldata")
.description("Prepare the calldata to be signed by the governance to migrate the funds from the legacy bridge")
.option("--tokens-list <tokensList>")
.option("--gas-per-token <gasPerToken>")
.option("--tokens-per-signature <tokensPerSignature>")
.option("--shared-bridge-addr <sharedBridgeAddr>")
.option("--legacy-bridge-addr <legacyBridgeAddr>")
.option("--era-chain-address <eraChainAddress>")
.option("--era-chain-id <eraChainId>")
.option("--delay <delay>")

.action(async (cmd) => {
const allTokens: string[] = JSON.parse(cmd.tokensList);
// Appending the ETH token to be migrated
allTokens.push("0x0000000000000000000000000000000000000001");

const tokensPerSignature = +cmd.tokensPerSignature;

const scheduleCalldatas = [];
const executeCalldatas = [];

for (let i = 0; i < allTokens.length; i += tokensPerSignature) {
const tokens = allTokens.slice(i, Math.min(i + tokensPerSignature, allTokens.length));
const { scheduleCalldata, executeCalldata } = await prepareGovernanceTokenMigrationCall(
tokens,
cmd.sharedBridgeAddr,
cmd.legacyBridgeAddr,
cmd.eraChainAddress,
cmd.eraChainId,
+cmd.gasPerToken,
+cmd.delay
);

scheduleCalldatas.push(scheduleCalldata);
executeCalldatas.push(executeCalldata);
}

console.log("Schedule operations to sign: ");
scheduleCalldatas.forEach((calldata) => console.log(calldata + "\n"));

console.log("Execute operations to sign: ");
executeCalldatas.forEach((calldata) => console.log(calldata + "\n"));
});

await program.parseAsync(process.argv);
}

main()
.then(() => process.exit(0))
.catch((err) => {
console.error("Error:", err);
process.exit(1);
});

async function loadAllConfirmedTokensFromAPI(l2Provider: Provider) {
const limit = 50;
const result = [];
let offset = 0;

// eslint-disable-next-line no-constant-condition
while (true) {
const tokens = await l2Provider.send("zks_getConfirmedTokens", [offset, limit]);
if (!tokens.length) {
return result;
}

tokens.forEach((token) => result.push(token.l1Address));
offset += limit;
}
}

async function loadAllConfirmedTokensFromL1(
l1Provider: ethers.providers.JsonRpcProvider,
bridgeAddress: string,
startBlock: number
) {
const blocksRange = 50000;
const endBlock = await l1Provider.getBlockNumber();
const abi = (await hardhat.artifacts.readArtifact("IL1ERC20Bridge")).abi;
const contract = new ethers.Contract(bridgeAddress, abi, l1Provider);
const filter = contract.filters.DepositInitiated();

const tokens = {};

while (startBlock <= endBlock) {
console.log("Querying blocks ", startBlock, " - ", Math.min(startBlock + blocksRange, endBlock));
const logs = await l1Provider.getLogs({
...filter,
fromBlock: startBlock,
toBlock: Math.min(startBlock + blocksRange, endBlock),
});
const deposits = logs.map((log) => contract.interface.parseLog(log));
deposits.forEach((dep) => {
if (!tokens[dep.args.l1Token]) {
console.log(dep.args.l1Token, " found!");
}
tokens[dep.args.l1Token] = true;
});

startBlock += blocksRange;
}

return Object.keys(tokens);
}

async function prepareGovernanceTokenMigrationCall(
tokens: string[],
l1SharedBridgeAddr: string,
l1LegacyBridgeAddr: string,
eraChainAddress: string,
eraChainId: number,
gasPerToken: number,
delay: number
) {
const governanceAbi = new ethers.utils.Interface((await hardhat.artifacts.readArtifact("IGovernance")).abi);
const sharedBridgeAbi = new ethers.utils.Interface((await hardhat.artifacts.readArtifact("L1SharedBridge")).abi);
const calls = tokens.map((token) => {
const target = token == utils.ETH_ADDRESS_IN_CONTRACTS ? eraChainAddress : l1LegacyBridgeAddr;

return {
target: l1SharedBridgeAddr,
value: 0,
data: sharedBridgeAbi.encodeFunctionData("safeTransferFundsFromLegacy", [token, target, eraChainId, gasPerToken]),
};
});
const governanceOp = {
calls,
predecessor: ethers.constants.HashZero,
salt: ethers.constants.HashZero,
};

const scheduleCalldata = governanceAbi.encodeFunctionData("scheduleTransparent", [governanceOp, delay]);
const executeCalldata = governanceAbi.encodeFunctionData("execute", [governanceOp]);

return {
scheduleCalldata,
executeCalldata,
};
}
6 changes: 4 additions & 2 deletions l1-contracts/src.ts/hyperchain-upgrade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -411,10 +411,11 @@ async function migrateAssets(deployer: Deployer) {
console.log("transferring Eth");
}
const sharedBridge = deployer.defaultSharedBridge(deployer.deployWallet);
const ethTransferData = sharedBridge.interface.encodeFunctionData("transferFundsFromLegacy", [
const ethTransferData = sharedBridge.interface.encodeFunctionData("safeTransferFundsFromLegacy", [
ADDRESS_ONE,
deployer.addresses.StateTransition.DiamondProxy,
deployer.chainId,
300_000,
]);
await deployer.executeUpgrade(deployer.addresses.Bridges.SharedBridgeProxy, 0, ethTransferData);

Expand All @@ -433,10 +434,11 @@ async function migrateAssets(deployer: Deployer) {
);
await mintTx.wait();

const daiTransferData = sharedBridge.interface.encodeFunctionData("transferFundsFromLegacy", [
const daiTransferData = sharedBridge.interface.encodeFunctionData("safeTransferFundsFromLegacy", [
altTokenAddress,
deployer.addresses.Bridges.ERC20BridgeProxy,
deployer.chainId,
300_000,
]);
// daiTransferData;
await deployer.executeUpgrade(deployer.addresses.Bridges.SharedBridgeProxy, 0, daiTransferData);
Expand Down

0 comments on commit fcee799

Please sign in to comment.