diff --git a/.gitignore b/.gitignore index adebc90..9d9d343 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,6 @@ node_modules # Hardhat Ignition default folder for deployments against a local node ignition/deployments/chain-31337 + +# Ignore the generated genesis.toml +genesis.toml diff --git a/README.md b/README.md index 1ffe593..ca2b66b 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # DUSK Migration Contract -This project contains the smart contract and related scripts for migrating DUSK tokens from ERC20/BEP20 to native DUSK. The project is built using Hardhat. +This project contains the smart contract and related scripts for migrating DUSK tokens from ERC20/BEP20 to native DUSK. It also contains the Dusk Mainnet Onramp contract. The project is built using Hardhat. **Migration Flow**: 1. User invokes `migrate()` with ERC20/BEP20 DUSK tokens and their Dusk mainnet Moonlight key. @@ -10,8 +10,8 @@ This project contains the smart contract and related scripts for migrating DUSK ## Overview The DUSK migration contract is designed to lock DUSK into the contract, and provide a receiving address on the DUSK side. It includes: -- **Smart Contracts**: A [Solidity migration contract](./contracts/DUSKMigration.sol) and an [ERC20 mock based on ERC20 DUSK](./contracts/ERC20Mock.sol) for testing. -- **Scripts**: Scripts for compiling contracts, extracting the ABI, listening to the migrate events and collecting past events. +- **Smart Contracts**: A [Solidity migration contract](./contracts/DUSKMigration.sol), an [ERC20 mock based on ERC20 DUSK](./contracts/ERC20Mock.sol) for testing and the [Dusk Mainnet Onramp contract](./contracts/DuskMainnetOnramp.sol). +- **Scripts**: Scripts for compiling contracts, extracting the ABI, listening to the migrate events, collecting past events and gathering genesis deposit/stake events. - **Tests**: Integration tests that test how the migrate function behaves. ## Clone repo @@ -62,6 +62,15 @@ npm run events:past This will dump an `.abi.json` file in the `contract` folder. +### Get Genesis Onramp events + +To get genesis deposit and stake events, and convert it to a `genesis.toml`, set up a `.env` file based on the `example.env` file and run: +```shell +npm run events:genesis +``` + +This will create a `genesis.toml` file in the root folder. + ### Run Tests To run the tests: diff --git a/example.env b/example.env index bd7589d..1134e87 100644 --- a/example.env +++ b/example.env @@ -1,2 +1,10 @@ PROVIDER_URL="https://sepolia.infura.io/v3/" DUSK_MIGRATION_CONTRACT_ADDRESS="0x" + +ETH_MAINNET_PROVIDER_URL="https://mainnet.infura.io/v3/" +ETH_ONRAMP_CONTRACT_ADDRESS="0x8787BbE53920B33411F7C9A91Ac321AF1ea1aa2d" +ETH_ONRAMP_DEPLOY_BLOCK="21445561" + +BSC_MAINNET_PROVIDER_URL="https://bsc-mainnet.infura.io/v3/" +BSC_ONRAMP_CONTRACT_ADDRESS="0x3886ab688feBfF60cE21E99251035F8E29Abca31" +BSC_ONRAMP_DEPLOY_BLOCK="45046348" diff --git a/package.json b/package.json index a2a1c3b..101e056 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,8 @@ "test:gas": "UPDATE_SNAPSHOT=1 yarn test --grep gas", "clean": "rm -rf ./artifacts ./cache ./typechain", "events:listen": "node ./scripts/listenEvents.js", - "events:past": "node ./scripts/pastEvents.js" + "events:past": "node ./scripts/pastEvents.js", + "events:genesis": "node ./scripts/genesisEvents.js" }, "author": "", "license": "ISC", @@ -21,4 +22,4 @@ "dependencies": { "@dotenvx/dotenvx": "^1.14.0" } -} \ No newline at end of file +} diff --git a/scripts/genesisEvents.js b/scripts/genesisEvents.js new file mode 100644 index 0000000..bf1e867 --- /dev/null +++ b/scripts/genesisEvents.js @@ -0,0 +1,148 @@ +require('@dotenvx/dotenvx').config(); +const { ethers } = require("ethers"); +const fs = require("fs"); + +// Chain metadata to combine both calls to Ethereum and BSC +const chains = [ + { + name: "Ethereum", + rpcUrl: process.env.ETH_MAINNET_PROVIDER_URL, + contractAddress: process.env.ETH_ONRAMP_CONTRACT_ADDRESS, + startBlock: process.env.ETH_ONRAMP_DEPLOY_BLOCK + }, + { + name: "Binance Smart Chain", + rpcUrl: process.env.BSC_MAINNET_PROVIDER_URL, + contractAddress: process.env.BSC_ONRAMP_CONTRACT_ADDRESS, + startBlock: process.env.BSC_ONRAMP_DEPLOY_BLOCK + }, +]; + +// Genesis event ABIs +const contractABI = [ + "event GenesisDeposit(address indexed from, uint256 amount, string targetAddress)", + "event GenesisStake(address indexed from, uint256 amount, string targetAddress)" +]; + +// Function to merge entries based on events. +// For example, to prevent two stake entries for the same key +function mergeEntries(entries, valueKey) { + const merged = {}; + + entries.forEach(({ address, [valueKey]: value }) => { + const numericValue = BigInt(value.replace(/_/g, "")); + if (!merged[address]) { + merged[address] = numericValue; + } else { + merged[address] += numericValue; + } + }); + + // Convert back to the required format + return Object.entries(merged).map(([address, value]) => ({ + address, + [valueKey]: value.toString().replace(/\B(?=(\d{3})+(?!\d))/g, "_"), + })); +} + +// Function to fetch events for a given chain configuration of the Onramp contract +async function fetchEvents(chain) { + const provider = new ethers.JsonRpcProvider(chain.rpcUrl); + const contract = new ethers.Contract(chain.contractAddress, contractABI, provider); + + const fromBlock = chain.startBlock; + const toBlock = "latest"; + + let stakeEntries = []; + let moonlightEntries = []; + + try { + // Fetch GenesisDeposit events + const depositFilter = contract.filters.GenesisDeposit(); + const depositEvents = await contract.queryFilter(depositFilter, BigInt(fromBlock), toBlock); + + // Fetch GenesisStake events + const stakeFilter = contract.filters.GenesisStake(); + const stakeEvents = await contract.queryFilter(stakeFilter, BigInt(fromBlock), toBlock); + + // Process GenesisDeposit events + depositEvents.forEach((event) => { + moonlightEntries.push({ + address: event.args.targetAddress, + balance: parseInt(event.args.amount.toString(), 10).toLocaleString("en-US").replace(/,/g, "_"), + }); + }); + + // Process GenesisStake events + stakeEvents.forEach((event) => { + stakeEntries.push({ + address: event.args.targetAddress, + amount: parseInt(event.args.amount.toString(), 10).toLocaleString("en-US").replace(/,/g, "_"), + }); + }); + + } catch (error) { + console.error(`Error fetching events on ${chain.name}:`, error); + } + + // Merge duplicate entries on a per event basis + const mergedStakeEntries = mergeEntries(stakeEntries, 'amount'); + const mergedMoonlightEntries = mergeEntries(moonlightEntries, 'balance'); + + return { stakeEntries: mergedStakeEntries, moonlightEntries: mergedMoonlightEntries }; +} + +// Custom TOML writer to handle our number formatting +function writeTOML(data) { + let tomlContent = ""; + + if (data.stake) { + data.stake.forEach((entry, index) => { + tomlContent += "[[stake]]\n"; + tomlContent += `address = '${entry.address}'\n`; + tomlContent += `amount = ${entry.amount}\n\n`; + }); + } + + if (data.moonlight_account) { + data.moonlight_account.forEach((entry) => { + tomlContent += "[[moonlight_account]]\n"; + tomlContent += `address = '${entry.address}'\n`; + tomlContent += `balance = ${entry.balance}\n\n`; + }); + } + + return tomlContent.trim(); +} + +async function main() { + let allStakeEntries = []; + let allMoonlightEntries = []; + + // Collect all GenesisDeposit and GenesisStake events for each chain config + for (const chain of chains) { + console.log(`Fetching events on ${chain.name}...`); + const { stakeEntries, moonlightEntries } = await fetchEvents(chain); + allStakeEntries = allStakeEntries.concat(stakeEntries); + allMoonlightEntries = allMoonlightEntries.concat(moonlightEntries); + } + + // Combine entries across chains to handle duplicate event entries globally + allStakeEntries = mergeEntries(allStakeEntries, 'amount'); + allMoonlightEntries = mergeEntries(allMoonlightEntries, 'balance'); + + // Create genesis data structure + const genesisData = { + stake: allStakeEntries, + moonlight_account: allMoonlightEntries, + }; + + // Generate TOML content + const tomlContent = writeTOML(genesisData); + + // Write TOML content to a file + fs.writeFileSync("genesis.toml", tomlContent); + console.log("Generated genesis.toml file."); +} + +main();