diff --git a/integration-tests/test/alt-l2/teleportation.spec.ts b/integration-tests/test/alt-l2/teleportation.spec.ts index ad54b8c73f..a37d8c0422 100644 --- a/integration-tests/test/alt-l2/teleportation.spec.ts +++ b/integration-tests/test/alt-l2/teleportation.spec.ts @@ -3,12 +3,12 @@ import { expect } from '@boba/teleportation/test/setup' /* External Imports */ import { ethers } from 'hardhat' import { - ContractFactory, + BigNumber, Contract, + ContractFactory, Signer, - BigNumber, - Wallet, utils, + Wallet, } from 'ethers' import { orderBy } from 'lodash' @@ -21,9 +21,12 @@ import { ChainInfo } from '@boba/teleportation/src/utils/types' /* Imports: Core */ import { TeleportationService } from '@boba/teleportation/src/service' -import { AppDataSource, historyDataRepository } from "@boba/teleportation/src/data-source"; -import { HistoryData } from '@boba/teleportation/src/entity/HistoryData' +import { + AppDataSource, + historyDataRepository, +} from '@boba/teleportation/src/data-source' import { OptimismEnv } from './shared/env' +import { getContractFactory, predeploys } from '@eth-optimism/contracts' describe('teleportation', () => { let env: OptimismEnv @@ -34,9 +37,14 @@ describe('teleportation', () => { let address1: string let selectedBobaChains: ChainInfo[] + let selectedBobaChainsBnb: ChainInfo[] const pollingInterval: number = 1000 const blockRangePerPolling = 1000 + const defaultMinDepositAmount = utils.parseEther('1') + const defaultMaxDepositAmount = utils.parseEther('100') + const defaultMaxTransferPerDay = utils.parseEther('100000') + before(async () => { env = await OptimismEnv.new() await AppDataSource.initialize() @@ -47,21 +55,26 @@ describe('teleportation', () => { wallet1 = env.l2Wallet_2 address1 = wallet1.address - await signer.sendTransaction({ to: wallet1.address, value: ethers.utils.parseEther('100'), }) }) + let chainId: number + let chainIdBnb: number let Factory__Teleportation: ContractFactory let Teleportation: Contract + let TeleportationBNB: Contract let Factory__L2BOBA: ContractFactory let L2BOBA: Contract + let L2BobaOnBobaBnb: Contract + let L2BNBOnBobaEth: Contract before(async () => { - const chainId = (await ethers.provider.getNetwork()).chainId + chainId = (await ethers.provider.getNetwork()).chainId + chainIdBnb = chainId + 1 Factory__Teleportation = new ethers.ContractFactory( TeleportationJson.abi, @@ -84,17 +97,25 @@ describe('teleportation', () => { 18 ) await L2BOBA.deployTransaction.wait() - await L2BOBA.transfer(address1, utils.parseEther('100000000')) // intialize the teleportation contract - await Teleportation.initialize( + await Teleportation.initialize() + // add the supported chain & token + await Teleportation.addSupportedToken( L2BOBA.address, - utils.parseEther('1'), - utils.parseEther('100') + chainId, + defaultMinDepositAmount, + defaultMaxDepositAmount, + defaultMaxTransferPerDay + ) + await Teleportation.addSupportedToken( + ethers.constants.AddressZero, + chainId, + defaultMinDepositAmount, + defaultMaxDepositAmount, + defaultMaxTransferPerDay ) - // add the supported chain - await Teleportation.addSupportedChain(chainId) // build payload selectedBobaChains = [ @@ -106,24 +127,34 @@ describe('teleportation', () => { name: 'localhost', teleportationAddress: Teleportation.address, height: 0, - BobaTokenAddress: L2BOBA.address, + supportedAssets: { + [L2BOBA.address]: 'BOBA', + [ethers.constants.AddressZero]: 'ETH', + }, }, + // bnb will be added in routing tests to have cleaner before hooks ] + selectedBobaChainsBnb = selectedBobaChains }) - const startTeleportationService = async () => { - const chainId = (await ethers.provider.getNetwork()).chainId - const teleportationService = new TeleportationService({ + const startTeleportationService = async (useBnb?: boolean) => { + const chainIdToUse = useBnb ? chainIdBnb : chainId + + return new TeleportationService({ l2RpcProvider: ethers.provider, - chainId, - teleportationAddress: Teleportation.address, - bobaTokenAddress: L2BOBA.address, + chainId: chainIdToUse, + teleportationAddress: useBnb + ? TeleportationBNB.address + : Teleportation.address, disburserWallet: wallet1, - selectedBobaChains, + selectedBobaChains: useBnb ? selectedBobaChainsBnb : selectedBobaChains, + // only defined one other for the routing tests (so idx 0 = own origin network) + ownSupportedAssets: useBnb + ? selectedBobaChains[0].supportedAssets + : selectedBobaChainsBnb[0].supportedAssets, pollingInterval, blockRangePerPolling, }) - return teleportationService } it('should create TeleportationService', async () => { @@ -133,7 +164,6 @@ describe('teleportation', () => { describe('unit function tests', () => { it('should get an event from Teleportation contract', async () => { - const chainId = (await ethers.provider.getNetwork()).chainId const teleportationService = await startTeleportationService() await teleportationService.init() @@ -141,7 +171,7 @@ describe('teleportation', () => { const events = await teleportationService._getEvents( Teleportation, - Teleportation.filters.BobaReceived(), + Teleportation.filters.AssetReceived(), 0, blockNumber ) @@ -149,7 +179,8 @@ describe('teleportation', () => { // deposit token await L2BOBA.approve(Teleportation.address, utils.parseEther('10')) - await Teleportation.connect(signer).teleportBOBA( + await Teleportation.connect(signer).teleportAsset( + L2BOBA.address, utils.parseEther('10'), chainId ) @@ -157,7 +188,7 @@ describe('teleportation', () => { const latestBlockNumber = await ethers.provider.getBlockNumber() const latestEvents = await teleportationService._getEvents( Teleportation, - Teleportation.filters.BobaReceived(), + Teleportation.filters.AssetReceived(), 0, latestBlockNumber ) @@ -171,35 +202,31 @@ describe('teleportation', () => { }) it('should send a disbursement TX for a single event', async () => { - const chainId = (await ethers.provider.getNetwork()).chainId const teleportationService = await startTeleportationService() await teleportationService.init() const blockNumber = await ethers.provider.getBlockNumber() const events = await teleportationService._getEvents( Teleportation, - Teleportation.filters.BobaReceived(), + Teleportation.filters.AssetReceived(), 0, blockNumber ) - expect(events.length).to.be.eq(1) - expect(events[0].args.sourceChainId).to.be.eq(chainId) - expect(events[0].args.toChainId).to.be.eq(chainId) - expect(events[0].args.depositId).to.be.eq(0, `Unexpected deposit ID: ${events[0].args.depositId}`) - expect(events[0].args.emitter).to.be.eq(signerAddr) - expect(events[0].args.amount).to.be.eq(utils.parseEther('10'), 'Amount unexpected') + expect(events.length).to.be.gt(0, 'Event length must be greater than 0') let disbursement = [] for (const event of events) { const sourceChainId = event.args.sourceChainId const depositId = event.args.depositId const amount = event.args.amount + const token = event.args.token const emitter = event.args.emitter disbursement = [ ...disbursement, { + token, amount: amount.toString(), addr: emitter, depositId: depositId.toNumber(), @@ -225,22 +252,21 @@ describe('teleportation', () => { utils.parseEther('10') ) - const amountDisbursements = await Teleportation - .connect(signer) - .totalDisbursements(chainId) + const amountDisbursements = await Teleportation.connect( + signer + ).totalDisbursements(chainId) expect(amountDisbursements).to.be.eq(1) }) it('should block the disbursement TX if it is already disbursed', async () => { - const chainId = (await ethers.provider.getNetwork()).chainId const teleportationService = await startTeleportationService() await teleportationService.init() const blockNumber = await ethers.provider.getBlockNumber() const events = await teleportationService._getEvents( Teleportation, - Teleportation.filters.BobaReceived(), + Teleportation.filters.AssetReceived(), 0, blockNumber ) @@ -251,10 +277,12 @@ describe('teleportation', () => { const depositId = event.args.depositId const amount = event.args.amount const emitter = event.args.emitter + const token = event.args.token disbursement = [ ...disbursement, { + token, amount: amount.toString(), addr: emitter, depositId: depositId.toNumber(), @@ -278,7 +306,6 @@ describe('teleportation', () => { }) it('should get events from Teleportation contract', async () => { - const chainId = (await ethers.provider.getNetwork()).chainId const teleportationService = await startTeleportationService() await teleportationService.init() @@ -287,7 +314,8 @@ describe('teleportation', () => { // deposit token for (let i = 0; i < 15; i++) { await L2BOBA.approve(Teleportation.address, utils.parseEther('10')) - await Teleportation.connect(signer).teleportBOBA( + await Teleportation.connect(signer).teleportAsset( + L2BOBA.address, utils.parseEther('10'), chainId ) @@ -296,7 +324,7 @@ describe('teleportation', () => { const endBlockNumber = await ethers.provider.getBlockNumber() const latestEvents = await teleportationService._getEvents( Teleportation, - Teleportation.filters.BobaReceived(), + Teleportation.filters.AssetReceived(), startBlockNumber, endBlockNumber ) @@ -305,14 +333,13 @@ describe('teleportation', () => { }) it('should slice events into chunks and send disbursements', async () => { - const chainId = (await ethers.provider.getNetwork()).chainId const teleportationService = await startTeleportationService() await teleportationService.init() const blockNumber = await ethers.provider.getBlockNumber() const events = await teleportationService._getEvents( Teleportation, - Teleportation.filters.BobaReceived(), + Teleportation.filters.AssetReceived(), 0, blockNumber ) @@ -320,6 +347,7 @@ describe('teleportation', () => { let disbursement = [] for (const event of events) { + const token = event.args.token const sourceChainId = event.args.sourceChainId const depositId = event.args.depositId const amount = event.args.amount @@ -329,6 +357,7 @@ describe('teleportation', () => { disbursement = [ ...disbursement, { + token, amount: amount.toString(), addr: emitter, depositId: depositId.toNumber(), @@ -362,13 +391,13 @@ describe('teleportation', () => { describe('global tests', () => { it('should watch Teleportation contract', async () => { - const chainId = (await ethers.provider.getNetwork()).chainId const teleportationService = await startTeleportationService() await teleportationService.init() // deposit token await L2BOBA.approve(Teleportation.address, utils.parseEther('11')) - await Teleportation.connect(signer).teleportBOBA( + await Teleportation.connect(signer).teleportAsset( + L2BOBA.address, utils.parseEther('11'), chainId ) @@ -387,6 +416,7 @@ describe('teleportation', () => { latestBlock ) expect(events.length).to.be.eq(2) + expect(events[1].args.token).to.be.eq(L2BOBA.address) expect(events[1].args.sourceChainId).to.be.eq(chainId) expect(events[1].args.toChainId).to.be.eq(chainId) expect(events[1].args.depositId).to.be.eq(16) @@ -395,7 +425,6 @@ describe('teleportation', () => { }) it('should disburse BOBA token for a single event', async () => { - const chainId = (await ethers.provider.getNetwork()).chainId const teleportationService = await startTeleportationService() await teleportationService.init() @@ -437,15 +466,15 @@ describe('teleportation', () => { expect(storedBlock).to.be.eq(latestBlock) }).retries(3) - it('should get all BobaReceived events', async () => { - const chainId = (await ethers.provider.getNetwork()).chainId + it('should get all AssetReceived events', async () => { const teleportationService = await startTeleportationService() await teleportationService.init() // deposit token await L2BOBA.approve(Teleportation.address, utils.parseEther('100')) for (let i = 0; i < 11; i++) { - await Teleportation.connect(signer).teleportBOBA( + await Teleportation.connect(signer).teleportAsset( + L2BOBA.address, utils.parseEther('1'), chainId ) @@ -468,7 +497,6 @@ describe('teleportation', () => { }) it('should disburse BOBA token for all events', async () => { - const chainId = (await ethers.provider.getNetwork()).chainId const teleportationService = await startTeleportationService() await teleportationService.init() @@ -511,8 +539,6 @@ describe('teleportation', () => { }).retries(3) it('should not disburse BOBA token if the data is reset', async () => { - - const chainId = (await ethers.provider.getNetwork()).chainId const teleportationService = await startTeleportationService() await teleportationService.init() @@ -541,4 +567,359 @@ describe('teleportation', () => { expect(preBOBABalance.sub(postBOBABalance)).to.be.eq(0) }) }) + + describe('asset routing', () => { + before(async () => { + TeleportationBNB = await Factory__Teleportation.deploy() + await Teleportation.deployTransaction.wait() + + // deploy other token for routing tests + L2BobaOnBobaBnb = await Factory__L2BOBA.deploy( + utils.parseEther('100000000000'), + 'BOBA', + 'BOBA', + 18 + ) + await L2BobaOnBobaBnb.deployTransaction.wait() + await L2BobaOnBobaBnb.transfer(address1, utils.parseEther('100000000')) + + // deploy other token for routing tests + L2BNBOnBobaEth = await Factory__L2BOBA.deploy( + utils.parseEther('100000000000'), + 'BNB', + 'BNB', + 18 + ) + await L2BNBOnBobaEth.deployTransaction.wait() + await L2BNBOnBobaEth.transfer(address1, utils.parseEther('100000000')) + + // intialize the teleportation contract + await TeleportationBNB.initialize() + + // add the supported chain & token + await TeleportationBNB.addSupportedToken( + L2BobaOnBobaBnb.address, + chainId, + defaultMinDepositAmount, + defaultMaxDepositAmount, + defaultMaxTransferPerDay + ) + await TeleportationBNB.addSupportedToken( + ethers.constants.AddressZero, + chainId, + defaultMinDepositAmount, + defaultMaxDepositAmount, + defaultMaxTransferPerDay + ) + + // add support on previous network + await Teleportation.addSupportedToken( + L2BNBOnBobaEth.address, + chainIdBnb, + defaultMinDepositAmount, + defaultMaxDepositAmount, + defaultMaxTransferPerDay + ) + await Teleportation.addSupportedToken( + L2BOBA.address, + chainIdBnb, + defaultMinDepositAmount, + defaultMaxDepositAmount, + defaultMaxTransferPerDay + ) + + console.log( + `Teleportation on ETH: ${Teleportation.address} / on BNB: ${TeleportationBNB.address}` + ) + + // mock BNB network & overwrite prev network + selectedBobaChains = [ + { + chainId: chainIdBnb, + url: 'http://localhost:8545', + provider: ethers.provider, + testnet: true, + name: 'localhost:bnb', + teleportationAddress: TeleportationBNB.address, + height: 0, + supportedAssets: { + [L2BobaOnBobaBnb.address]: 'BOBA', + [ethers.constants.AddressZero]: 'BNB', // simulate BNB for native to token teleport + }, + }, + ] + selectedBobaChainsBnb = [ + { + chainId, + url: 'http://localhost:8545', + provider: ethers.provider, + testnet: true, + name: 'localhost', + teleportationAddress: Teleportation.address, + height: 0, + supportedAssets: { + [L2BOBA.address]: 'BOBA', + [ethers.constants.AddressZero]: 'ETH', + [L2BNBOnBobaEth.address]: 'BNB', + }, + }, + ] + }) + + it('teleport BOBA as token from chain A (e.g. BNB) to chain B (ETH)', async () => { + const teleportationServiceBnb = await startTeleportationService(true) + await teleportationServiceBnb.init() + + // deposit token + const preBlockNumber = await ethers.provider.getBlockNumber() + await L2BobaOnBobaBnb.connect(signer).approve( + TeleportationBNB.address, + utils.parseEther('10') + ) + await TeleportationBNB.connect(signer).teleportAsset( + L2BobaOnBobaBnb.address, + utils.parseEther('10'), + chainId // toChainId + ) + + const blockNumber = await ethers.provider.getBlockNumber() + const events = await teleportationServiceBnb._getEvents( + TeleportationBNB, + TeleportationBNB.filters.AssetReceived(), + preBlockNumber, + blockNumber + ) + + expect(events.length).to.be.gt(0, 'Event length must be greater than 0') + + const teleportationServiceEth = await startTeleportationService(false) + await teleportationServiceEth.init() + + let disbursement = [] + for (const event of events) { + const sourceChainId = chainIdBnb // event.args.sourceChainId.toNumber() -> (is correct, but we were mocking a fake chainId for testing) + const depositId = event.args.depositId + const amount = event.args.amount + const token = event.args.token + const emitter = event.args.emitter + + const receivingChainTokenAddr = + teleportationServiceEth._getSupportedDestChainTokenAddrBySourceChainTokenAddr( + token, + sourceChainId + ) + expect(receivingChainTokenAddr).to.be.eq( + L2BOBA.address, + 'BOBA token address on BNB not correctly routed' + ) + + disbursement = [ + ...disbursement, + { + token: receivingChainTokenAddr, + amount: amount.toString(), + addr: emitter, + depositId: depositId.toNumber(), + sourceChainId: sourceChainId.toString(), + }, + ] + } + + console.log('Added disbursement: ', disbursement) + + disbursement = orderBy(disbursement, ['depositId'], ['asc']) + + const preBOBABalance = await L2BOBA.balanceOf(address1) + const preSignerBOBABalance = await L2BOBA.balanceOf(signerAddr) + + await teleportationServiceEth._disburseTx( + disbursement, + chainId, + blockNumber + ) + + const postBOBABalance = await L2BOBA.balanceOf(address1) + const postSignerBOBABalance = await L2BOBA.balanceOf(signerAddr) + + expect(preBOBABalance.sub(postBOBABalance)).to.be.eq( + utils.parseEther('10') + ) + expect(postSignerBOBABalance.sub(preSignerBOBABalance)).to.be.eq( + utils.parseEther('10') + ) + }) + + it('teleport BNB as native from chain B (e.g. BNB) to chain A (ETH) as wrapped token', async () => { + const teleportationService = await startTeleportationService(true) + await teleportationService.init() + + // deposit token + const preBlockNumber = await ethers.provider.getBlockNumber() + await TeleportationBNB.connect(signer).teleportAsset( + ethers.constants.AddressZero, // send native BNB + utils.parseEther('10'), + chainId, // toChainId + { value: utils.parseEther('10') } + ) + + const blockNumber = await ethers.provider.getBlockNumber() + const events = await teleportationService._getEvents( + TeleportationBNB, + TeleportationBNB.filters.AssetReceived(), + preBlockNumber, + blockNumber + ) + + expect(events.length).to.be.gt(0, 'Event length must be greater than 0') + + const teleportationServiceEth = await startTeleportationService(false) + await teleportationServiceEth.init() + + let disbursement = [] + for (const event of events) { + const sourceChainId = chainIdBnb // event.args.sourceChainId.toNumber() -> (is correct, but we were mocking a fake chainId for testing) + const depositId = event.args.depositId + const amount = event.args.amount + const token = event.args.token + const emitter = event.args.emitter + + const receivingChainTokenAddr = + teleportationServiceEth._getSupportedDestChainTokenAddrBySourceChainTokenAddr( + token, + sourceChainId + ) + expect(receivingChainTokenAddr).to.be.eq( + L2BNBOnBobaEth.address, + 'BNB token address on Boba ETH not correctly routed' + ) + + disbursement = [ + ...disbursement, + { + token: receivingChainTokenAddr, + amount: amount.toString(), + addr: emitter, + depositId: depositId.toNumber(), + sourceChainId: sourceChainId.toString(), + }, + ] + + console.log('Added disbursement native: ', disbursement) + } + + disbursement = orderBy(disbursement, ['depositId'], ['asc']) + + const preBNBBalance = await L2BNBOnBobaEth.balanceOf(address1) + const preSignerBNBBalance = await L2BNBOnBobaEth.balanceOf(signerAddr) + + await teleportationServiceEth._disburseTx( + disbursement, + chainId, + blockNumber + ) + + const postBNBBalance = await L2BNBOnBobaEth.balanceOf(address1) + const postSignerBNBBalance = await L2BNBOnBobaEth.balanceOf(signerAddr) + + expect(preBNBBalance.sub(postBNBBalance)).to.be.eq(utils.parseEther('10')) + expect(postSignerBNBBalance.sub(preSignerBNBBalance)).to.be.eq( + utils.parseEther('10') + ) + }) + + it('teleport BNB as token from chain A (ETH) to chain B (e.g. BNB) as native asset', async () => { + const teleportationService = await startTeleportationService(false) + await teleportationService.init() + + // deposit token + const preBlockNumber = await ethers.provider.getBlockNumber() + + await L2BNBOnBobaEth.approve( + Teleportation.address, + utils.parseEther('10') + ) + await Teleportation.connect(signer).teleportAsset( + L2BNBOnBobaEth.address, // send BNB as token + utils.parseEther('10'), + chainIdBnb // toChainId + ) + + const blockNumber = await ethers.provider.getBlockNumber() + const events = await teleportationService._getEvents( + Teleportation, + Teleportation.filters.AssetReceived(), + preBlockNumber, + blockNumber + ) + + expect(events.length).to.be.gt(0, 'Event length must be greater than 0') + + const teleportationServiceBnb = await startTeleportationService(true) + await teleportationServiceBnb.init() + + let disbursement = [] + for (const event of events) { + const sourceChainId = event.args.sourceChainId.toNumber() + const depositId = await TeleportationBNB.totalDisbursements(chainId) // event.args.depositId --> correct, but we used a fake chainId to simulate Bnb so we need to correct depositId here + const amount = event.args.amount + const token = event.args.token + const emitter = event.args.emitter + + const receivingChainTokenAddr = + teleportationServiceBnb._getSupportedDestChainTokenAddrBySourceChainTokenAddr( + token, + sourceChainId + ) + expect(receivingChainTokenAddr).to.be.eq( + ethers.constants.AddressZero, + 'BNB native asset on Boba BNB not correctly routed' + ) + + disbursement = [ + ...disbursement, + { + token: receivingChainTokenAddr, + amount: amount.toString(), + addr: emitter, + depositId, // artificially increment necessary, as we mocked fake chainId in previous test (to avoid unexpected next depositId) + sourceChainId: sourceChainId.toString(), + }, + ] + } + + disbursement = orderBy(disbursement, ['depositId'], ['asc']) + + const bnbChainInfo = selectedBobaChains.find( + (c) => c.chainId === chainIdBnb + ) + if (!bnbChainInfo) { + throw new Error('BNB provider not configured!') + } + + const preBNBBalance = await bnbChainInfo.provider.getBalance(address1) + const preSignerBNBBalance = await bnbChainInfo.provider.getBalance( + signerAddr + ) + + await teleportationServiceBnb._disburseTx( + disbursement, + chainIdBnb, + blockNumber + ) + + const postBNBBalance = await bnbChainInfo.provider.getBalance(address1) + const postSignerBNBBalance = await bnbChainInfo.provider.getBalance( + signerAddr + ) + + expect(preBNBBalance.sub(postBNBBalance)).to.be.closeTo( + utils.parseEther('9.08'), + utils.parseEther('10.02') // gas used by disburse transaction(s) + ) + expect(postSignerBNBBalance.sub(preSignerBNBBalance)).to.be.closeTo( + utils.parseEther('9.08'), + utils.parseEther('10.02') + ) + }) + }) }) diff --git a/integration-tests/test/eth-l2/teleportation.spec.ts b/integration-tests/test/eth-l2/teleportation.spec.ts index ad54b8c73f..a37d8c0422 100644 --- a/integration-tests/test/eth-l2/teleportation.spec.ts +++ b/integration-tests/test/eth-l2/teleportation.spec.ts @@ -3,12 +3,12 @@ import { expect } from '@boba/teleportation/test/setup' /* External Imports */ import { ethers } from 'hardhat' import { - ContractFactory, + BigNumber, Contract, + ContractFactory, Signer, - BigNumber, - Wallet, utils, + Wallet, } from 'ethers' import { orderBy } from 'lodash' @@ -21,9 +21,12 @@ import { ChainInfo } from '@boba/teleportation/src/utils/types' /* Imports: Core */ import { TeleportationService } from '@boba/teleportation/src/service' -import { AppDataSource, historyDataRepository } from "@boba/teleportation/src/data-source"; -import { HistoryData } from '@boba/teleportation/src/entity/HistoryData' +import { + AppDataSource, + historyDataRepository, +} from '@boba/teleportation/src/data-source' import { OptimismEnv } from './shared/env' +import { getContractFactory, predeploys } from '@eth-optimism/contracts' describe('teleportation', () => { let env: OptimismEnv @@ -34,9 +37,14 @@ describe('teleportation', () => { let address1: string let selectedBobaChains: ChainInfo[] + let selectedBobaChainsBnb: ChainInfo[] const pollingInterval: number = 1000 const blockRangePerPolling = 1000 + const defaultMinDepositAmount = utils.parseEther('1') + const defaultMaxDepositAmount = utils.parseEther('100') + const defaultMaxTransferPerDay = utils.parseEther('100000') + before(async () => { env = await OptimismEnv.new() await AppDataSource.initialize() @@ -47,21 +55,26 @@ describe('teleportation', () => { wallet1 = env.l2Wallet_2 address1 = wallet1.address - await signer.sendTransaction({ to: wallet1.address, value: ethers.utils.parseEther('100'), }) }) + let chainId: number + let chainIdBnb: number let Factory__Teleportation: ContractFactory let Teleportation: Contract + let TeleportationBNB: Contract let Factory__L2BOBA: ContractFactory let L2BOBA: Contract + let L2BobaOnBobaBnb: Contract + let L2BNBOnBobaEth: Contract before(async () => { - const chainId = (await ethers.provider.getNetwork()).chainId + chainId = (await ethers.provider.getNetwork()).chainId + chainIdBnb = chainId + 1 Factory__Teleportation = new ethers.ContractFactory( TeleportationJson.abi, @@ -84,17 +97,25 @@ describe('teleportation', () => { 18 ) await L2BOBA.deployTransaction.wait() - await L2BOBA.transfer(address1, utils.parseEther('100000000')) // intialize the teleportation contract - await Teleportation.initialize( + await Teleportation.initialize() + // add the supported chain & token + await Teleportation.addSupportedToken( L2BOBA.address, - utils.parseEther('1'), - utils.parseEther('100') + chainId, + defaultMinDepositAmount, + defaultMaxDepositAmount, + defaultMaxTransferPerDay + ) + await Teleportation.addSupportedToken( + ethers.constants.AddressZero, + chainId, + defaultMinDepositAmount, + defaultMaxDepositAmount, + defaultMaxTransferPerDay ) - // add the supported chain - await Teleportation.addSupportedChain(chainId) // build payload selectedBobaChains = [ @@ -106,24 +127,34 @@ describe('teleportation', () => { name: 'localhost', teleportationAddress: Teleportation.address, height: 0, - BobaTokenAddress: L2BOBA.address, + supportedAssets: { + [L2BOBA.address]: 'BOBA', + [ethers.constants.AddressZero]: 'ETH', + }, }, + // bnb will be added in routing tests to have cleaner before hooks ] + selectedBobaChainsBnb = selectedBobaChains }) - const startTeleportationService = async () => { - const chainId = (await ethers.provider.getNetwork()).chainId - const teleportationService = new TeleportationService({ + const startTeleportationService = async (useBnb?: boolean) => { + const chainIdToUse = useBnb ? chainIdBnb : chainId + + return new TeleportationService({ l2RpcProvider: ethers.provider, - chainId, - teleportationAddress: Teleportation.address, - bobaTokenAddress: L2BOBA.address, + chainId: chainIdToUse, + teleportationAddress: useBnb + ? TeleportationBNB.address + : Teleportation.address, disburserWallet: wallet1, - selectedBobaChains, + selectedBobaChains: useBnb ? selectedBobaChainsBnb : selectedBobaChains, + // only defined one other for the routing tests (so idx 0 = own origin network) + ownSupportedAssets: useBnb + ? selectedBobaChains[0].supportedAssets + : selectedBobaChainsBnb[0].supportedAssets, pollingInterval, blockRangePerPolling, }) - return teleportationService } it('should create TeleportationService', async () => { @@ -133,7 +164,6 @@ describe('teleportation', () => { describe('unit function tests', () => { it('should get an event from Teleportation contract', async () => { - const chainId = (await ethers.provider.getNetwork()).chainId const teleportationService = await startTeleportationService() await teleportationService.init() @@ -141,7 +171,7 @@ describe('teleportation', () => { const events = await teleportationService._getEvents( Teleportation, - Teleportation.filters.BobaReceived(), + Teleportation.filters.AssetReceived(), 0, blockNumber ) @@ -149,7 +179,8 @@ describe('teleportation', () => { // deposit token await L2BOBA.approve(Teleportation.address, utils.parseEther('10')) - await Teleportation.connect(signer).teleportBOBA( + await Teleportation.connect(signer).teleportAsset( + L2BOBA.address, utils.parseEther('10'), chainId ) @@ -157,7 +188,7 @@ describe('teleportation', () => { const latestBlockNumber = await ethers.provider.getBlockNumber() const latestEvents = await teleportationService._getEvents( Teleportation, - Teleportation.filters.BobaReceived(), + Teleportation.filters.AssetReceived(), 0, latestBlockNumber ) @@ -171,35 +202,31 @@ describe('teleportation', () => { }) it('should send a disbursement TX for a single event', async () => { - const chainId = (await ethers.provider.getNetwork()).chainId const teleportationService = await startTeleportationService() await teleportationService.init() const blockNumber = await ethers.provider.getBlockNumber() const events = await teleportationService._getEvents( Teleportation, - Teleportation.filters.BobaReceived(), + Teleportation.filters.AssetReceived(), 0, blockNumber ) - expect(events.length).to.be.eq(1) - expect(events[0].args.sourceChainId).to.be.eq(chainId) - expect(events[0].args.toChainId).to.be.eq(chainId) - expect(events[0].args.depositId).to.be.eq(0, `Unexpected deposit ID: ${events[0].args.depositId}`) - expect(events[0].args.emitter).to.be.eq(signerAddr) - expect(events[0].args.amount).to.be.eq(utils.parseEther('10'), 'Amount unexpected') + expect(events.length).to.be.gt(0, 'Event length must be greater than 0') let disbursement = [] for (const event of events) { const sourceChainId = event.args.sourceChainId const depositId = event.args.depositId const amount = event.args.amount + const token = event.args.token const emitter = event.args.emitter disbursement = [ ...disbursement, { + token, amount: amount.toString(), addr: emitter, depositId: depositId.toNumber(), @@ -225,22 +252,21 @@ describe('teleportation', () => { utils.parseEther('10') ) - const amountDisbursements = await Teleportation - .connect(signer) - .totalDisbursements(chainId) + const amountDisbursements = await Teleportation.connect( + signer + ).totalDisbursements(chainId) expect(amountDisbursements).to.be.eq(1) }) it('should block the disbursement TX if it is already disbursed', async () => { - const chainId = (await ethers.provider.getNetwork()).chainId const teleportationService = await startTeleportationService() await teleportationService.init() const blockNumber = await ethers.provider.getBlockNumber() const events = await teleportationService._getEvents( Teleportation, - Teleportation.filters.BobaReceived(), + Teleportation.filters.AssetReceived(), 0, blockNumber ) @@ -251,10 +277,12 @@ describe('teleportation', () => { const depositId = event.args.depositId const amount = event.args.amount const emitter = event.args.emitter + const token = event.args.token disbursement = [ ...disbursement, { + token, amount: amount.toString(), addr: emitter, depositId: depositId.toNumber(), @@ -278,7 +306,6 @@ describe('teleportation', () => { }) it('should get events from Teleportation contract', async () => { - const chainId = (await ethers.provider.getNetwork()).chainId const teleportationService = await startTeleportationService() await teleportationService.init() @@ -287,7 +314,8 @@ describe('teleportation', () => { // deposit token for (let i = 0; i < 15; i++) { await L2BOBA.approve(Teleportation.address, utils.parseEther('10')) - await Teleportation.connect(signer).teleportBOBA( + await Teleportation.connect(signer).teleportAsset( + L2BOBA.address, utils.parseEther('10'), chainId ) @@ -296,7 +324,7 @@ describe('teleportation', () => { const endBlockNumber = await ethers.provider.getBlockNumber() const latestEvents = await teleportationService._getEvents( Teleportation, - Teleportation.filters.BobaReceived(), + Teleportation.filters.AssetReceived(), startBlockNumber, endBlockNumber ) @@ -305,14 +333,13 @@ describe('teleportation', () => { }) it('should slice events into chunks and send disbursements', async () => { - const chainId = (await ethers.provider.getNetwork()).chainId const teleportationService = await startTeleportationService() await teleportationService.init() const blockNumber = await ethers.provider.getBlockNumber() const events = await teleportationService._getEvents( Teleportation, - Teleportation.filters.BobaReceived(), + Teleportation.filters.AssetReceived(), 0, blockNumber ) @@ -320,6 +347,7 @@ describe('teleportation', () => { let disbursement = [] for (const event of events) { + const token = event.args.token const sourceChainId = event.args.sourceChainId const depositId = event.args.depositId const amount = event.args.amount @@ -329,6 +357,7 @@ describe('teleportation', () => { disbursement = [ ...disbursement, { + token, amount: amount.toString(), addr: emitter, depositId: depositId.toNumber(), @@ -362,13 +391,13 @@ describe('teleportation', () => { describe('global tests', () => { it('should watch Teleportation contract', async () => { - const chainId = (await ethers.provider.getNetwork()).chainId const teleportationService = await startTeleportationService() await teleportationService.init() // deposit token await L2BOBA.approve(Teleportation.address, utils.parseEther('11')) - await Teleportation.connect(signer).teleportBOBA( + await Teleportation.connect(signer).teleportAsset( + L2BOBA.address, utils.parseEther('11'), chainId ) @@ -387,6 +416,7 @@ describe('teleportation', () => { latestBlock ) expect(events.length).to.be.eq(2) + expect(events[1].args.token).to.be.eq(L2BOBA.address) expect(events[1].args.sourceChainId).to.be.eq(chainId) expect(events[1].args.toChainId).to.be.eq(chainId) expect(events[1].args.depositId).to.be.eq(16) @@ -395,7 +425,6 @@ describe('teleportation', () => { }) it('should disburse BOBA token for a single event', async () => { - const chainId = (await ethers.provider.getNetwork()).chainId const teleportationService = await startTeleportationService() await teleportationService.init() @@ -437,15 +466,15 @@ describe('teleportation', () => { expect(storedBlock).to.be.eq(latestBlock) }).retries(3) - it('should get all BobaReceived events', async () => { - const chainId = (await ethers.provider.getNetwork()).chainId + it('should get all AssetReceived events', async () => { const teleportationService = await startTeleportationService() await teleportationService.init() // deposit token await L2BOBA.approve(Teleportation.address, utils.parseEther('100')) for (let i = 0; i < 11; i++) { - await Teleportation.connect(signer).teleportBOBA( + await Teleportation.connect(signer).teleportAsset( + L2BOBA.address, utils.parseEther('1'), chainId ) @@ -468,7 +497,6 @@ describe('teleportation', () => { }) it('should disburse BOBA token for all events', async () => { - const chainId = (await ethers.provider.getNetwork()).chainId const teleportationService = await startTeleportationService() await teleportationService.init() @@ -511,8 +539,6 @@ describe('teleportation', () => { }).retries(3) it('should not disburse BOBA token if the data is reset', async () => { - - const chainId = (await ethers.provider.getNetwork()).chainId const teleportationService = await startTeleportationService() await teleportationService.init() @@ -541,4 +567,359 @@ describe('teleportation', () => { expect(preBOBABalance.sub(postBOBABalance)).to.be.eq(0) }) }) + + describe('asset routing', () => { + before(async () => { + TeleportationBNB = await Factory__Teleportation.deploy() + await Teleportation.deployTransaction.wait() + + // deploy other token for routing tests + L2BobaOnBobaBnb = await Factory__L2BOBA.deploy( + utils.parseEther('100000000000'), + 'BOBA', + 'BOBA', + 18 + ) + await L2BobaOnBobaBnb.deployTransaction.wait() + await L2BobaOnBobaBnb.transfer(address1, utils.parseEther('100000000')) + + // deploy other token for routing tests + L2BNBOnBobaEth = await Factory__L2BOBA.deploy( + utils.parseEther('100000000000'), + 'BNB', + 'BNB', + 18 + ) + await L2BNBOnBobaEth.deployTransaction.wait() + await L2BNBOnBobaEth.transfer(address1, utils.parseEther('100000000')) + + // intialize the teleportation contract + await TeleportationBNB.initialize() + + // add the supported chain & token + await TeleportationBNB.addSupportedToken( + L2BobaOnBobaBnb.address, + chainId, + defaultMinDepositAmount, + defaultMaxDepositAmount, + defaultMaxTransferPerDay + ) + await TeleportationBNB.addSupportedToken( + ethers.constants.AddressZero, + chainId, + defaultMinDepositAmount, + defaultMaxDepositAmount, + defaultMaxTransferPerDay + ) + + // add support on previous network + await Teleportation.addSupportedToken( + L2BNBOnBobaEth.address, + chainIdBnb, + defaultMinDepositAmount, + defaultMaxDepositAmount, + defaultMaxTransferPerDay + ) + await Teleportation.addSupportedToken( + L2BOBA.address, + chainIdBnb, + defaultMinDepositAmount, + defaultMaxDepositAmount, + defaultMaxTransferPerDay + ) + + console.log( + `Teleportation on ETH: ${Teleportation.address} / on BNB: ${TeleportationBNB.address}` + ) + + // mock BNB network & overwrite prev network + selectedBobaChains = [ + { + chainId: chainIdBnb, + url: 'http://localhost:8545', + provider: ethers.provider, + testnet: true, + name: 'localhost:bnb', + teleportationAddress: TeleportationBNB.address, + height: 0, + supportedAssets: { + [L2BobaOnBobaBnb.address]: 'BOBA', + [ethers.constants.AddressZero]: 'BNB', // simulate BNB for native to token teleport + }, + }, + ] + selectedBobaChainsBnb = [ + { + chainId, + url: 'http://localhost:8545', + provider: ethers.provider, + testnet: true, + name: 'localhost', + teleportationAddress: Teleportation.address, + height: 0, + supportedAssets: { + [L2BOBA.address]: 'BOBA', + [ethers.constants.AddressZero]: 'ETH', + [L2BNBOnBobaEth.address]: 'BNB', + }, + }, + ] + }) + + it('teleport BOBA as token from chain A (e.g. BNB) to chain B (ETH)', async () => { + const teleportationServiceBnb = await startTeleportationService(true) + await teleportationServiceBnb.init() + + // deposit token + const preBlockNumber = await ethers.provider.getBlockNumber() + await L2BobaOnBobaBnb.connect(signer).approve( + TeleportationBNB.address, + utils.parseEther('10') + ) + await TeleportationBNB.connect(signer).teleportAsset( + L2BobaOnBobaBnb.address, + utils.parseEther('10'), + chainId // toChainId + ) + + const blockNumber = await ethers.provider.getBlockNumber() + const events = await teleportationServiceBnb._getEvents( + TeleportationBNB, + TeleportationBNB.filters.AssetReceived(), + preBlockNumber, + blockNumber + ) + + expect(events.length).to.be.gt(0, 'Event length must be greater than 0') + + const teleportationServiceEth = await startTeleportationService(false) + await teleportationServiceEth.init() + + let disbursement = [] + for (const event of events) { + const sourceChainId = chainIdBnb // event.args.sourceChainId.toNumber() -> (is correct, but we were mocking a fake chainId for testing) + const depositId = event.args.depositId + const amount = event.args.amount + const token = event.args.token + const emitter = event.args.emitter + + const receivingChainTokenAddr = + teleportationServiceEth._getSupportedDestChainTokenAddrBySourceChainTokenAddr( + token, + sourceChainId + ) + expect(receivingChainTokenAddr).to.be.eq( + L2BOBA.address, + 'BOBA token address on BNB not correctly routed' + ) + + disbursement = [ + ...disbursement, + { + token: receivingChainTokenAddr, + amount: amount.toString(), + addr: emitter, + depositId: depositId.toNumber(), + sourceChainId: sourceChainId.toString(), + }, + ] + } + + console.log('Added disbursement: ', disbursement) + + disbursement = orderBy(disbursement, ['depositId'], ['asc']) + + const preBOBABalance = await L2BOBA.balanceOf(address1) + const preSignerBOBABalance = await L2BOBA.balanceOf(signerAddr) + + await teleportationServiceEth._disburseTx( + disbursement, + chainId, + blockNumber + ) + + const postBOBABalance = await L2BOBA.balanceOf(address1) + const postSignerBOBABalance = await L2BOBA.balanceOf(signerAddr) + + expect(preBOBABalance.sub(postBOBABalance)).to.be.eq( + utils.parseEther('10') + ) + expect(postSignerBOBABalance.sub(preSignerBOBABalance)).to.be.eq( + utils.parseEther('10') + ) + }) + + it('teleport BNB as native from chain B (e.g. BNB) to chain A (ETH) as wrapped token', async () => { + const teleportationService = await startTeleportationService(true) + await teleportationService.init() + + // deposit token + const preBlockNumber = await ethers.provider.getBlockNumber() + await TeleportationBNB.connect(signer).teleportAsset( + ethers.constants.AddressZero, // send native BNB + utils.parseEther('10'), + chainId, // toChainId + { value: utils.parseEther('10') } + ) + + const blockNumber = await ethers.provider.getBlockNumber() + const events = await teleportationService._getEvents( + TeleportationBNB, + TeleportationBNB.filters.AssetReceived(), + preBlockNumber, + blockNumber + ) + + expect(events.length).to.be.gt(0, 'Event length must be greater than 0') + + const teleportationServiceEth = await startTeleportationService(false) + await teleportationServiceEth.init() + + let disbursement = [] + for (const event of events) { + const sourceChainId = chainIdBnb // event.args.sourceChainId.toNumber() -> (is correct, but we were mocking a fake chainId for testing) + const depositId = event.args.depositId + const amount = event.args.amount + const token = event.args.token + const emitter = event.args.emitter + + const receivingChainTokenAddr = + teleportationServiceEth._getSupportedDestChainTokenAddrBySourceChainTokenAddr( + token, + sourceChainId + ) + expect(receivingChainTokenAddr).to.be.eq( + L2BNBOnBobaEth.address, + 'BNB token address on Boba ETH not correctly routed' + ) + + disbursement = [ + ...disbursement, + { + token: receivingChainTokenAddr, + amount: amount.toString(), + addr: emitter, + depositId: depositId.toNumber(), + sourceChainId: sourceChainId.toString(), + }, + ] + + console.log('Added disbursement native: ', disbursement) + } + + disbursement = orderBy(disbursement, ['depositId'], ['asc']) + + const preBNBBalance = await L2BNBOnBobaEth.balanceOf(address1) + const preSignerBNBBalance = await L2BNBOnBobaEth.balanceOf(signerAddr) + + await teleportationServiceEth._disburseTx( + disbursement, + chainId, + blockNumber + ) + + const postBNBBalance = await L2BNBOnBobaEth.balanceOf(address1) + const postSignerBNBBalance = await L2BNBOnBobaEth.balanceOf(signerAddr) + + expect(preBNBBalance.sub(postBNBBalance)).to.be.eq(utils.parseEther('10')) + expect(postSignerBNBBalance.sub(preSignerBNBBalance)).to.be.eq( + utils.parseEther('10') + ) + }) + + it('teleport BNB as token from chain A (ETH) to chain B (e.g. BNB) as native asset', async () => { + const teleportationService = await startTeleportationService(false) + await teleportationService.init() + + // deposit token + const preBlockNumber = await ethers.provider.getBlockNumber() + + await L2BNBOnBobaEth.approve( + Teleportation.address, + utils.parseEther('10') + ) + await Teleportation.connect(signer).teleportAsset( + L2BNBOnBobaEth.address, // send BNB as token + utils.parseEther('10'), + chainIdBnb // toChainId + ) + + const blockNumber = await ethers.provider.getBlockNumber() + const events = await teleportationService._getEvents( + Teleportation, + Teleportation.filters.AssetReceived(), + preBlockNumber, + blockNumber + ) + + expect(events.length).to.be.gt(0, 'Event length must be greater than 0') + + const teleportationServiceBnb = await startTeleportationService(true) + await teleportationServiceBnb.init() + + let disbursement = [] + for (const event of events) { + const sourceChainId = event.args.sourceChainId.toNumber() + const depositId = await TeleportationBNB.totalDisbursements(chainId) // event.args.depositId --> correct, but we used a fake chainId to simulate Bnb so we need to correct depositId here + const amount = event.args.amount + const token = event.args.token + const emitter = event.args.emitter + + const receivingChainTokenAddr = + teleportationServiceBnb._getSupportedDestChainTokenAddrBySourceChainTokenAddr( + token, + sourceChainId + ) + expect(receivingChainTokenAddr).to.be.eq( + ethers.constants.AddressZero, + 'BNB native asset on Boba BNB not correctly routed' + ) + + disbursement = [ + ...disbursement, + { + token: receivingChainTokenAddr, + amount: amount.toString(), + addr: emitter, + depositId, // artificially increment necessary, as we mocked fake chainId in previous test (to avoid unexpected next depositId) + sourceChainId: sourceChainId.toString(), + }, + ] + } + + disbursement = orderBy(disbursement, ['depositId'], ['asc']) + + const bnbChainInfo = selectedBobaChains.find( + (c) => c.chainId === chainIdBnb + ) + if (!bnbChainInfo) { + throw new Error('BNB provider not configured!') + } + + const preBNBBalance = await bnbChainInfo.provider.getBalance(address1) + const preSignerBNBBalance = await bnbChainInfo.provider.getBalance( + signerAddr + ) + + await teleportationServiceBnb._disburseTx( + disbursement, + chainIdBnb, + blockNumber + ) + + const postBNBBalance = await bnbChainInfo.provider.getBalance(address1) + const postSignerBNBBalance = await bnbChainInfo.provider.getBalance( + signerAddr + ) + + expect(preBNBBalance.sub(postBNBBalance)).to.be.closeTo( + utils.parseEther('9.08'), + utils.parseEther('10.02') // gas used by disburse transaction(s) + ) + expect(postSignerBNBBalance.sub(preSignerBNBBalance)).to.be.closeTo( + utils.parseEther('9.08'), + utils.parseEther('10.02') + ) + }) + }) }) diff --git a/ops/.env-template b/ops/.env-template new file mode 100644 index 0000000000..16c1d9769c --- /dev/null +++ b/ops/.env-template @@ -0,0 +1,4 @@ +# Encrypted Disburser Key +TELEPORTATION_DISBURSER_KEY_ENC= +# Disburser Key AWS ID +TELEPORTATION_DISBURSER_KEY_AWS_ID= diff --git a/ops/docker-compose-side.yml b/ops/docker-compose-side.yml index 44329db7b8..894352f4fe 100644 --- a/ops/docker-compose-side.yml +++ b/ops/docker-compose-side.yml @@ -62,6 +62,7 @@ services: depends_on: - l2geth - teleportation_db + - kms image: bobanetwork/teleportation:latest build: context: .. @@ -69,6 +70,10 @@ services: target: teleportation deploy: replicas: 0 + environment: + # TODO: Further compare KMS setup with BatchSubmitter + TELEPORTATION_DISBURSER_KEY_ID: "${TELEPORTATION_DISBURSER_KEY_ID}" + TELEPORTATION_DISBURSER_KEY_ENC: "${TELEPORTATION_DISBURSER_KEY_ENC}" teleportation_db: image: postgres diff --git a/packages/boba/contracts/contracts/Teleportation.sol b/packages/boba/contracts/contracts/Teleportation.sol index 67b03f81a9..a9ddf2f829 100644 --- a/packages/boba/contracts/contracts/Teleportation.sol +++ b/packages/boba/contracts/contracts/Teleportation.sol @@ -5,15 +5,19 @@ pragma solidity 0.8.9; import '@openzeppelin/contracts/utils/math/SafeMath.sol'; import '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol'; import "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/utils/MulticallUpgradeable.sol"; +import '@openzeppelin/contracts/utils/Address.sol'; /** * @title Teleportation * - * Shout out to optimisim for providing the inspiration for this contract: - * https://github.com/ethereum-optimism/optimism/blob/develop/packages/contracts/contracts/L1/teleportr/TeleportrDeposit.sol - * https://github.com/ethereum-optimism/optimism/blob/develop/packages/contracts/contracts/L2/teleportr/TeleportrDisburser.sol + * Bridge the native asset or whitelisted ERC20 tokens between whitelisted networks (L2's/L1's). + * + * @notice The contract itself emits events and locks the funds to be bridged/teleported. These events are then picked up by a backend service that releases the corresponding token or native asset on the destination network. Withdrawal periods (e.g. for optimistic rollups) are not handled by the contract itself, but would be handled on the Teleportation service if deemed necessary. + * @dev Implementation of the Teleportation service can be found at https://github.com/bobanetwork/boba within /packages/boba/teleportation if not moved. */ -contract Teleportation is PausableUpgradeable { +contract Teleportation is PausableUpgradeable, MulticallUpgradeable { + using Address for address; using SafeMath for uint256; using SafeERC20 for IERC20; @@ -21,6 +25,7 @@ contract Teleportation is PausableUpgradeable { * Struct * **************/ struct Disbursement { + address token; uint256 amount; address addr; uint256 sourceChainId; @@ -32,64 +37,79 @@ contract Teleportation is PausableUpgradeable { Disbursement disbursement; } + struct SupportedToken { + bool supported; + // The minimum amount that needs to be deposited in a receive. + uint256 minDepositAmount; + // The maximum amount that can be deposited in a receive. + uint256 maxDepositAmount; + // set maximum amount of tokens that can be transferred in 24 hours + uint256 maxTransferAmountPerDay; + // The total amount of tokens transferred in 24 hours + uint256 transferredAmount; + // The timestamp of the checkpoint + uint256 transferTimestampCheckPoint; + } + /************* * Variables * *************/ + /// @dev Wallet that is being used to release teleported assets on the destination network. address public disburser; + + /// @dev General owner wallet to change configurations. address public owner; - address public BobaTokenAddress; - - mapping(uint256 => bool) public supportedChains; - - // The minimum amount that be deposited in a receive. - uint256 public minDepositAmount; - // The maximum amount that be deposited in a receive. - uint256 public maxDepositAmount; - // The total number of successful deposits received. - mapping (uint256 => uint256) public totalDeposits; - /// The total number of disbursements processed. - mapping (uint256 => uint256) public totalDisbursements; - - // set maximum amount of tokens can be transferred in 24 hours - uint256 public maxTransferAmountPerDay; - // The total amount of tokens transferred in 24 hours - uint256 public transferredAmount; - // The timestamp of the checkpoint - uint256 public transferTimestampCheckPoint; - // depositId to failed status and disbursement info - mapping (uint256 => FailedNativeDisbursement) public failedNativeDisbursements; + + /// @dev Assets and networks to be supported. ZeroAddress for native asset + /// {assetAddress} => {targetChainId} => {tokenConfig} + mapping(address => mapping(uint256 => SupportedToken)) public supportedTokens; + + /// @dev The total number of successful deposits received. + mapping(uint256 => uint256) public totalDeposits; + + /// @dev The total number of disbursements processed. + mapping(uint256 => uint256) public totalDisbursements; + + // @dev depositId to failed status and disbursement info + mapping(uint256 => FailedNativeDisbursement) public failedNativeDisbursements; /******************** * Events * ********************/ event MinDepositAmountSet( + /* @dev Zero Address = native asset **/ + address token, + uint256 toChainId, uint256 previousAmount, uint256 newAmount ); event MaxDepositAmountSet( + /* @dev Zero Address = native asset **/ + address token, + uint256 toChainId, uint256 previousAmount, uint256 newAmount ); event MaxTransferAmountPerDaySet( + address token, + uint256 toChainId, uint256 previousAmount, uint256 newAmount ); - event NativeBOBABalanceWithdrawn( + event AssetBalanceWithdrawn( + address indexed token, address indexed owner, uint256 balance ); - event BOBABalanceWithdrawn( - address indexed owner, - uint256 balance - ); - - event BobaReceived( + event AssetReceived( + /** @dev Must be ZeroAddress for nativeAsset */ + address token, uint256 sourceChainId, uint256 indexed toChainId, uint256 indexed depositId, @@ -100,10 +120,12 @@ contract Teleportation is PausableUpgradeable { event DisbursementSuccess( uint256 indexed depositId, address indexed to, + address indexed token, uint256 amount, uint256 sourceChainId ); + /** @dev only for native assets */ event DisbursementFailed( uint256 indexed depositId, address indexed to, @@ -111,6 +133,14 @@ contract Teleportation is PausableUpgradeable { uint256 sourceChainId ); + /** @dev Only for native assets */ + event DisbursementRetrySuccess( + uint256 indexed depositId, + address indexed to, + uint256 amount, + uint256 sourceChainId + ); + event DisburserTransferred( address newDisburser ); @@ -119,18 +149,12 @@ contract Teleportation is PausableUpgradeable { address newOwner ); - event ChainSupported( - uint256 indexed chainId, + event TokenSupported( + address indexed token, + uint256 indexed toChainId, bool supported ); - event DisbursementRetrySuccess( - uint256 indexed depositId, - address indexed to, - uint256 amount, - uint256 sourceChainId - ); - /********************** * Function Modifiers * **********************/ @@ -146,22 +170,12 @@ contract Teleportation is PausableUpgradeable { } modifier onlyNotInitialized() { - require(address(BobaTokenAddress) == address(0), "Contract has been initialized"); + require(address(disburser) == address(0), "Contract has been initialized"); _; } modifier onlyInitialized() { - require(address(BobaTokenAddress) != address(0), "Contract has not yet been initialized"); - _; - } - - modifier onlyAltL2s() { - require(address(BobaTokenAddress) == 0x4200000000000000000000000000000000000006, "Only alt L2s can call this function"); - _; - } - - modifier onlyNotAltL2s() { - require(address(BobaTokenAddress) != 0x4200000000000000000000000000000000000006, "only non alt L2s"); + require(address(disburser) != address(0), "Contract has not yet been initialized"); _; } @@ -169,67 +183,56 @@ contract Teleportation is PausableUpgradeable { * Public Functions * ********************/ - /** - * @dev Initialize this contract. - * - * @param _BobaTokenAddress BOBA token address - * @param _minDepositAmount The initial minimum deposit amount. - * @param _maxDepositAmount The initial maximum deposit amount. - */ - function initialize( - address _BobaTokenAddress, - uint256 _minDepositAmount, - uint256 _maxDepositAmount - ) - external - onlyNotInitialized() - initializer() - { - require(_BobaTokenAddress != address(0), "zero address not allowed"); - require(_minDepositAmount > 0 && _maxDepositAmount > 0 && _minDepositAmount <= _maxDepositAmount, "incorrect min/max deposit"); - minDepositAmount = _minDepositAmount; - maxDepositAmount = _maxDepositAmount; - BobaTokenAddress = _BobaTokenAddress; + /// @dev Initialize this contract + function initialize() external onlyNotInitialized() initializer() { disburser = msg.sender; owner = msg.sender; - // set maximum amount of tokens can be transferred in 24 hours - transferTimestampCheckPoint = block.timestamp; - maxTransferAmountPerDay = 100_000e18; - require(_maxDepositAmount <= maxTransferAmountPerDay, "max deposit amount cannot be more than daily limit"); - __Context_init_unchained(); __Pausable_init_unchained(); + __Multicall_init_unchained(); - emit MinDepositAmountSet(0, _minDepositAmount); - emit MaxDepositAmountSet(0, _maxDepositAmount); - emit MaxTransferAmountPerDaySet(0, maxTransferAmountPerDay); emit DisburserTransferred(owner); emit OwnershipTransferred(owner); } /** - * @dev add the support between this chain and the given chain. - * - * @param _chainId The target chain Id to support. - */ - function addSupportedChain(uint256 _chainId) external onlyOwner() onlyInitialized() { - require(supportedChains[_chainId] == false, "Chain is already supported"); - supportedChains[_chainId] = true; - - emit ChainSupported(_chainId, true); + * @dev Add support of a specific ERC20 token on this network. + * + * @param _token Token address to support or ZeroAddress for native + */ + function addSupportedToken(address _token, uint256 _toChainId, uint256 _minDepositAmount, uint256 _maxDepositAmount, uint256 _maxTransferAmountPerDay) public onlyOwner() onlyInitialized() { + require(supportedTokens[_token][_toChainId].supported == false, "Already supported"); + // Not added ERC165 as implemented for L1 ERC20 + require(address(0) == _token || Address.isContract(_token), "Not contract or native"); + // doesn't ensure it's ERC20 + + require(_minDepositAmount > 0 && _minDepositAmount <= _maxDepositAmount, "incorrect min/max deposit"); + // set maximum amount of tokens that can be transferred in 24 hours + require(_maxDepositAmount <= _maxTransferAmountPerDay, "max deposit amount more than daily limit"); + + supportedTokens[_token][_toChainId] = SupportedToken(true, _minDepositAmount, _maxDepositAmount, _maxTransferAmountPerDay, 0, block.timestamp); + + emit TokenSupported(_token, _toChainId, true); + emit MinDepositAmountSet(_token, _toChainId, 0, _minDepositAmount); + emit MaxDepositAmountSet(_token, _toChainId, 0, _maxDepositAmount); + emit MaxTransferAmountPerDaySet(_token, _toChainId, 0, _maxTransferAmountPerDay); } /** - * @dev remove the support between this chain and the given chain. + * @dev remove the support for a specific token. * - * @param _chainId The target chain Id not to support. + * @param _token The token not to support. */ - function removeSupportedChain(uint256 _chainId) external onlyOwner() onlyInitialized() { - require(supportedChains[_chainId] == true, "Chain is already not supported"); - supportedChains[_chainId] = false; - - emit ChainSupported(_chainId, false); + function removeSupportedToken(address _token, uint256 _toChainId) external onlyOwner() onlyInitialized() { + require(supportedTokens[_token][_toChainId].supported == true, "Already not supported"); + delete supportedTokens[_token][_toChainId].supported; + delete supportedTokens[_token][_toChainId].minDepositAmount; + delete supportedTokens[_token][_toChainId].maxDepositAmount; + delete supportedTokens[_token][_toChainId].maxTransferAmountPerDay; + // we might want to keep transferredAmount & lastTimestamp in case it is being added (shortly) after again + + emit TokenSupported(_token, _toChainId, false); } /** @@ -238,65 +241,39 @@ contract Teleportation is PausableUpgradeable { * minDepositAmount, the amount is greater than the current * maxDepositAmount. * - * @param _amount The amount of BOBA to deposit. + * @param _token ERC20 address of the token to deposit. + * @param _amount The amount of token or native asset to deposit (must be the same as msg.value if native asset) * @param _toChainId The destination chain ID. */ - function teleportBOBA(uint256 _amount, uint256 _toChainId) - external - onlyNotAltL2s() - whenNotPaused() + function teleportAsset(address _token, uint256 _amount, uint256 _toChainId) + external + payable + whenNotPaused() { - require(_amount >= minDepositAmount, "Deposit amount is too small"); - require(_amount <= maxDepositAmount, "Deposit amount is too big"); - require(supportedChains[_toChainId], "Target chain is not supported"); + SupportedToken memory supToken = supportedTokens[_token][_toChainId]; + require(supToken.supported == true, "Token or chain not supported"); + require(_amount >= supToken.minDepositAmount, "Deposit amount too small"); + require(_amount <= supToken.maxDepositAmount, "Deposit amount too big"); + // minimal workaround to keep logic concise + require(address(0) != _token || (address(0) == _token && _amount == msg.value), "Native amount invalid"); // check if the total amount transferred is smaller than the maximum amount of tokens can be transferred in 24 hours // if it's out of 24 hours, reset the transferred amount to 0 and set the transferTimestampCheckPoint to the current time - if (block.timestamp < transferTimestampCheckPoint + 86400) { - transferredAmount += _amount; - require(transferredAmount <= maxTransferAmountPerDay, "max amount per day exceeded"); + if (block.timestamp < supToken.transferTimestampCheckPoint + 86400) { + supToken.transferredAmount += _amount; + require(supToken.transferredAmount <= supToken.maxTransferAmountPerDay, "max amount per day exceeded"); } else { - transferredAmount = _amount; - require(transferredAmount <= maxTransferAmountPerDay, "max amount per day exceeded"); - transferTimestampCheckPoint = block.timestamp; + supToken.transferredAmount = _amount; + require(supToken.transferredAmount <= supToken.maxTransferAmountPerDay, "max amount per day exceeded"); + supToken.transferTimestampCheckPoint = block.timestamp; } - IERC20(BobaTokenAddress).safeTransferFrom(msg.sender, address(this), _amount); - - emit BobaReceived(block.chainid, _toChainId, totalDeposits[_toChainId], msg.sender, _amount); - totalDeposits[_toChainId] += 1; - } - - /** - * @dev Accepts deposits that will be disbursed to the sender's address on target L2. - * The method reverts if the amount is less than the current - * minDepositAmount, the amount is greater than the current - * maxDepositAmount. - * - * @param _toChainId The destination chain ID. - */ - function teleportNativeBOBA(uint256 _toChainId) - external - payable - onlyAltL2s() - whenNotPaused() - { - require(msg.value >= minDepositAmount, "Deposit amount is too small"); - require(msg.value <= maxDepositAmount, "Deposit amount is too big"); - require(supportedChains[_toChainId], "Target chain is not supported"); - - // check if the total amount transferred is smaller than the maximum amount of tokens can be transferred in 24 hours - // if it's out of 24 hours, reset the transferred amount to 0 and set the transferTimestampCheckPoint to the current time - if (block.timestamp < transferTimestampCheckPoint + 86400) { - transferredAmount += msg.value; - require(transferredAmount <= maxTransferAmountPerDay, "max amount per day exceeded"); - } else { - transferredAmount = msg.value; - require(transferredAmount <= maxTransferAmountPerDay, "max amount per day exceeded"); - transferTimestampCheckPoint = block.timestamp; + supportedTokens[_token][_toChainId] = supToken; + if (_token != address(0)) { + IERC20(_token).safeTransferFrom(msg.sender, address(this), _amount); } - emit BobaReceived(block.chainid, _toChainId, totalDeposits[_toChainId], msg.sender, msg.value); + emit AssetReceived(_token, block.chainid, _toChainId, totalDeposits[_toChainId], msg.sender, _amount); totalDeposits[_toChainId] += 1; } @@ -310,113 +287,72 @@ contract Teleportation is PausableUpgradeable { * * @param _disbursements A list of Disbursements to process. */ - function disburseNativeBOBA(Disbursement[] calldata _disbursements) - external - payable - onlyDisburser() - onlyAltL2s() - whenNotPaused() + function disburseAsset(Disbursement[] calldata _disbursements) + external + payable + onlyDisburser() + whenNotPaused() { // Ensure there are disbursements to process. uint256 _numDisbursements = _disbursements.length; require(_numDisbursements > 0, "No disbursements"); - // Ensure the amount sent in the transaction is equal to the sum of the - // disbursements. - uint256 _totalDisbursed = 0; - for (uint256 i = 0; i < _numDisbursements; i++) { - _totalDisbursed += _disbursements[i].amount; - } - - // Ensure the balance is enough to cover the disbursements. - require(_totalDisbursed == msg.value, "Disbursement total != amount sent"); - // Process disbursements. + uint remainingValue = msg.value; for (uint256 i = 0; i < _numDisbursements; i++) { + uint256 _amount = _disbursements[i].amount; address _addr = _disbursements[i].addr; uint256 _sourceChainId = _disbursements[i].sourceChainId; uint256 _depositId = _disbursements[i].depositId; + address _token = _disbursements[i].token; + + // Bidirectional support expected + require(supportedTokens[_token][_sourceChainId].supported, "Token or chain not supported"); // Ensure the depositId matches our expected value. require(_depositId == totalDisbursements[_sourceChainId], "Unexpected next deposit id"); - require(supportedChains[_sourceChainId], "Source chain is not supported"); totalDisbursements[_sourceChainId] += 1; - // Deliver the disbursement amount to the receiver. If the - // disbursement fails, the amount will be kept by the contract - // rather than reverting to prevent blocking progress on other - // disbursements. - - // slither-disable-next-line calls-loop,reentrancy-events - (bool success, ) = _addr.call{ gas: 3000, value: _amount }(""); - if (success) emit DisbursementSuccess(_depositId, _addr, _amount, _sourceChainId); - else { - failedNativeDisbursements[_depositId] = FailedNativeDisbursement(true, _disbursements[i]); - emit DisbursementFailed(_depositId, _addr, _amount, _sourceChainId); + // ensure amount sent in the tx is equal to disbursement (moved into loop to ensure token flexibility) + if (_token == address(0)) { + require(_amount <= remainingValue, "Disbursement total != amount sent"); + remainingValue -= _amount; + } else { + IERC20(_token).safeTransferFrom(msg.sender, address(this), _amount); } - } - } - - /** - * @dev Accepts a list of Disbursements and forwards the amount paid to - * the contract to each recipient. The method reverts if there are zero - * disbursements, the total amount to forward differs from the amount sent - * in the transaction, or the _nextDepositId is unexpected. Failed - * disbursements will not cause the method to revert, but will instead be - * held by the contract and availabe for the owner to withdraw. - * - * @param _disbursements A list of Disbursements to process. - */ - function disburseBOBA(Disbursement[] calldata _disbursements) - external - payable - onlyDisburser() - onlyNotAltL2s() - whenNotPaused() - { - // Ensure there are disbursements to process. - uint256 _numDisbursements = _disbursements.length; - require(_numDisbursements > 0, "No disbursements"); - - // Ensure the amount sent in the transaction is equal to the sum of the - // disbursements. - uint256 _totalDisbursed = 0; - for (uint256 i = 0; i < _numDisbursements; i++) { - _totalDisbursed += _disbursements[i].amount; - } - IERC20(BobaTokenAddress).safeTransferFrom(msg.sender, address(this), _totalDisbursed); - - // Process disbursements. - for (uint256 i = 0; i < _numDisbursements; i++) { - uint256 _amount = _disbursements[i].amount; - address _addr = _disbursements[i].addr; - uint256 _sourceChainId = _disbursements[i].sourceChainId; - uint256 _depositId = _disbursements[i].depositId; - - // Ensure the depositId matches our expected value. - require(_depositId == totalDisbursements[_sourceChainId], "Unexpected next deposit id"); - require(supportedChains[_sourceChainId], "Source chain is not supported"); - totalDisbursements[_sourceChainId] += 1; - - // slither-disable-next-line calls-loop,reentrancy-events - IERC20(BobaTokenAddress).safeTransfer(_addr, _amount); - emit DisbursementSuccess(_depositId, _addr, _amount, _sourceChainId); + if (_token == address(0)) { + // Deliver the disbursement amount to the receiver. If the + // disbursement fails, the amount will be kept by the contract + // rather than reverting to prevent blocking progress on other + // disbursements. + + // slither-disable-next-line calls-loop,reentrancy-events + (bool success,) = _addr.call{gas: 3000, value: _amount}(""); + if (success) emit DisbursementSuccess(_depositId, _addr, _token, _amount, _sourceChainId); + else { + failedNativeDisbursements[_depositId] = FailedNativeDisbursement(true, _disbursements[i]); + emit DisbursementFailed(_depositId, _addr, _amount, _sourceChainId); + } + } else { + // slither-disable-next-line calls-loop,reentrancy-events + IERC20(_token).safeTransfer(_addr, _amount); + } + emit DisbursementSuccess(_depositId, _addr, _token, _amount, _sourceChainId); } } /** - * @dev Retry native Boba disbursement if it failed previously + * @dev Retry native disbursement if it failed previously. Only applies to native disbursements bc. of low-level call. * * @param _depositIds A list of DepositIds to process. */ - function retryDisburseNativeBOBA(uint256[] memory _depositIds) - external - payable - onlyDisburser() - onlyAltL2s() - whenNotPaused() + function retryDisburseNative(uint256[] memory _depositIds) + external + payable + onlyDisburser() + whenNotPaused() { // Ensure there are disbursements to process. uint256 _numDisbursements = _depositIds.length; @@ -427,13 +363,13 @@ contract Teleportation is PausableUpgradeable { // Process disbursements. for (uint256 i = 0; i < _numDisbursements; i++) { FailedNativeDisbursement storage failedDisbursement = failedNativeDisbursements[_depositIds[i]]; - require(failedDisbursement.failed, "DepositId is not a failed disbursement"); + require(failedDisbursement.failed, "DepositId not failed disbursement"); uint256 _amount = failedDisbursement.disbursement.amount; address _addr = failedDisbursement.disbursement.addr; uint256 _sourceChainId = failedDisbursement.disbursement.sourceChainId; // slither-disable-next-line calls-loop,reentrancy-events - (bool success, ) = _addr.call{ gas: 3000, value: _amount }(""); + (bool success,) = _addr.call{gas: 3000, value: _amount}(""); if (success) { failedNativeDisbursements[_depositIds[i]].failed = false; emit DisbursementRetrySuccess(_depositIds[i], _addr, _amount, _sourceChainId); @@ -462,30 +398,22 @@ contract Teleportation is PausableUpgradeable { /** * @dev Sends the contract's current balance to the owner. */ - function withdrawNativeBOBABalance() - external - onlyOwner() - onlyInitialized() - onlyAltL2s() + function withdrawBalance(address _token) + external + onlyOwner() + onlyInitialized() { - uint256 _balance = address(this).balance; - (bool sent,) = owner.call{gas: 2300, value: _balance}(""); - require(sent, "Failed to send Ether"); - emit NativeBOBABalanceWithdrawn(owner, _balance); - } - - /** - * @dev Sends the contract's current balance to the owner. - */ - function withdrawBOBABalance() - external - onlyOwner() - onlyInitialized() - onlyNotAltL2s() - { - uint256 _balance = IERC20(BobaTokenAddress).balanceOf(address(this)); - IERC20(BobaTokenAddress).safeTransfer(owner, _balance); - emit BOBABalanceWithdrawn(owner, _balance); + if (address(0) == _token) { + uint256 _balance = address(this).balance; + (bool sent,) = owner.call{gas: 2300, value: _balance}(""); + require(sent, "Failed to send Ether"); + emit AssetBalanceWithdrawn(_token, owner, _balance); + } else { + // no supportedToken check in case of generally lost tokens + uint256 _balance = IERC20(_token).balanceOf(address(this)); + IERC20(_token).safeTransfer(owner, _balance); + emit AssetBalanceWithdrawn(_token, owner, _balance); + } } /** @@ -496,8 +424,8 @@ contract Teleportation is PausableUpgradeable { function transferDisburser( address _newDisburser ) - external - onlyOwner() + external + onlyOwner() { require(_newDisburser != address(0), 'New disburser cannot be the zero address'); disburser = _newDisburser; @@ -512,47 +440,64 @@ contract Teleportation is PausableUpgradeable { function transferOwnership( address _newOwner ) - external - onlyOwner() + external + onlyOwner() { require(_newOwner != address(0), 'New owner cannot be the zero address'); owner = _newOwner; emit OwnershipTransferred(_newOwner); } - /** * @notice Sets the minimum amount that can be deposited in a receive. * + * @param _token configure for which token or ZeroAddress for native * @param _minDepositAmount The new minimum deposit amount. */ - function setMinAmount(uint256 _minDepositAmount) external onlyOwner() { - require(_minDepositAmount > 0 && _minDepositAmount <= maxDepositAmount, "incorrect min deposit amount"); - uint256 pastMinDepositAmount = minDepositAmount; - minDepositAmount = _minDepositAmount; - emit MinDepositAmountSet(pastMinDepositAmount, minDepositAmount); + function setMinAmount(address _token, uint256 _toChainId, uint256 _minDepositAmount) external onlyOwner() { + SupportedToken memory supToken = supportedTokens[_token][_toChainId]; + require(supToken.supported, "Token or chain not supported"); + require(_minDepositAmount > 0 && _minDepositAmount <= supToken.maxDepositAmount, "incorrect min deposit amount"); + + uint256 pastMinDepositAmount = supToken.minDepositAmount; + supportedTokens[_token][_toChainId].minDepositAmount = _minDepositAmount; + + emit MinDepositAmountSet(_token, _toChainId, pastMinDepositAmount, _minDepositAmount); } /** * @dev Sets the maximum amount that can be deposited in a receive. * + * @param _token configure for which token or ZeroAddr for native asset + * @param _toChainId target chain id to set configuration for * @param _maxDepositAmount The new maximum deposit amount. */ - function setMaxAmount(uint256 _maxDepositAmount) external onlyOwner() { - require(_maxDepositAmount > 0 && minDepositAmount <= _maxDepositAmount, "incorrect max deposit amount"); - uint256 pastMaxDepositAmount = maxDepositAmount; - maxDepositAmount = _maxDepositAmount; - emit MaxDepositAmountSet(pastMaxDepositAmount, maxDepositAmount); + function setMaxAmount(address _token, uint256 _toChainId, uint256 _maxDepositAmount) external onlyOwner() { + SupportedToken memory supToken = supportedTokens[_token][_toChainId]; + require(supToken.supported, "Token or chain not supported"); + require(_maxDepositAmount <= supToken.maxTransferAmountPerDay, "max deposit amount more than daily limit"); + require(_maxDepositAmount > 0 && _maxDepositAmount >= supToken.minDepositAmount, "incorrect max deposit amount"); + uint256 pastMaxDepositAmount = supToken.maxDepositAmount; + + supportedTokens[_token][_toChainId].maxDepositAmount = _maxDepositAmount; + emit MaxDepositAmountSet(_token, _toChainId, pastMaxDepositAmount, _maxDepositAmount); } /** * @dev Sets maximum amount of disbursements that can be processed in a day * + * @param _token Token or native asset (ZeroAddr) to set value for * @param _maxTransferAmountPerDay The new maximum daily transfer amount. */ - function setMaxTransferAmountPerDay(uint256 _maxTransferAmountPerDay) external onlyOwner() { - uint256 pastMaxTransferAmountPerDay = maxTransferAmountPerDay; - maxTransferAmountPerDay = _maxTransferAmountPerDay; - emit MaxDepositAmountSet(pastMaxTransferAmountPerDay, maxTransferAmountPerDay); + function setMaxTransferAmountPerDay(address _token, uint256 _toChainId, uint256 _maxTransferAmountPerDay) external onlyOwner() { + + SupportedToken memory supToken = supportedTokens[_token][_toChainId]; + require(supToken.supported, "Token or chain not supported"); + require(_maxTransferAmountPerDay > 0 && _maxTransferAmountPerDay >= supToken.maxDepositAmount, "incorrect daily limit"); + uint256 pastMaxTransferAmountPerDay = supToken.maxTransferAmountPerDay; + + supportedTokens[_token][_toChainId].maxTransferAmountPerDay = _maxTransferAmountPerDay; + + emit MaxTransferAmountPerDaySet(_token, _toChainId, pastMaxTransferAmountPerDay, _maxTransferAmountPerDay); } } diff --git a/packages/boba/contracts/contracts/test-helpers/L1ERC20.sol b/packages/boba/contracts/contracts/test-helpers/L1ERC20.sol index b08a87bf95..af29c7f265 100644 --- a/packages/boba/contracts/contracts/test-helpers/L1ERC20.sol +++ b/packages/boba/contracts/contracts/test-helpers/L1ERC20.sol @@ -26,4 +26,4 @@ contract L1ERC20 is ERC20 { function decimals() public view virtual override returns (uint8) { return _decimals; } -} \ No newline at end of file +} diff --git a/packages/boba/contracts/deploy/020-Teleportation.deploy.ts b/packages/boba/contracts/deploy/020-Teleportation.deploy.ts index 9a83b79fa3..02e0ac75a3 100644 --- a/packages/boba/contracts/deploy/020-Teleportation.deploy.ts +++ b/packages/boba/contracts/deploy/020-Teleportation.deploy.ts @@ -53,16 +53,8 @@ const deployFn: DeployFunction = async (hre) => { Proxy__Teleportation.address, (hre as any).deployConfig.deployer_l2 ) - await Proxy__Teleportation.initialize( - L2BOBA.address, - utils.parseEther('1'), - utils.parseEther('100') - ) - await Teleportation.initialize( - L2BOBA.address, - utils.parseEther('1'), - utils.parseEther('100') - ) + await Proxy__Teleportation.initialize() + await Teleportation.initialize() console.log(`Proxy__Teleportation initialized`) await registerBobaAddress( @@ -75,6 +67,10 @@ const deployFn: DeployFunction = async (hre) => { 'Teleportation', Teleportation.address ) + + console.log( + `Proxy__Teleportation (${Proxy__Teleportation.address}) & Teleportation (${Teleportation.address}) registered.` + ) } deployFn.tags = ['Proxy__Teleportation', 'Teleportation', 'required'] diff --git a/packages/boba/contracts/test/endToEndTests/teleportation.spec.ts b/packages/boba/contracts/test/endToEndTests/teleportation.spec.ts index c20ed6904a..b962a366e0 100644 --- a/packages/boba/contracts/test/endToEndTests/teleportation.spec.ts +++ b/packages/boba/contracts/test/endToEndTests/teleportation.spec.ts @@ -5,6 +5,7 @@ import { ethers } from 'hardhat' import { Contract, Signer, BigNumber, utils } from 'ethers' let L2Boba: Contract +let RandomERC20: Contract let Teleportation: Contract let Proxy__Teleportation: Contract @@ -13,9 +14,14 @@ let signer2: Signer let signerAddress: string let signer2Address: string +const chainId31337 = '31337' +const chainId4 = '4' const initialSupply = utils.parseEther('10000000000') const tokenName = 'BOBA' const tokenSymbol = 'BOBA' +const defaultMinDepositAmount = ethers.utils.parseEther('1') +const defaultMaxDepositAmount = ethers.utils.parseEther('100000') +const defaultMaxDailyLimit = ethers.utils.parseEther('100000') const getGasFeeFromLastestBlock = async (provider: any): Promise => { const blockNumber = await provider.getBlockNumber() @@ -27,1017 +33,1780 @@ const getGasFeeFromLastestBlock = async (provider: any): Promise => { return gasUsed.mul(gasPrice) } -describe('BOBA Teleportation Tests', async () => { - describe('Ethereum L2 - BOBA is not the native token', () => { - before(async () => { - signer = (await ethers.getSigners())[0] - signer2 = (await ethers.getSigners())[1] - signerAddress = await signer.getAddress() - signer2Address = await signer2.getAddress() - - L2Boba = await ( - await ethers.getContractFactory('L1ERC20') - ).deploy(initialSupply, tokenName, tokenSymbol, 18) +describe('Asset Teleportation Tests', async () => { + describe('Teleport asset tests', async () => { + beforeEach(async () => { + // Not added by default anymore + await Proxy__Teleportation.addSupportedToken( + ethers.constants.AddressZero, + chainId4, + defaultMinDepositAmount, + defaultMaxDepositAmount, + defaultMaxDailyLimit + ) + let supToken = await Proxy__Teleportation.supportedTokens( + ethers.constants.AddressZero, + chainId4 + ) + expect(supToken[0]).to.eq(true) + expect(supToken[1]).to.eq(defaultMinDepositAmount) + expect(supToken[2]).to.eq(defaultMaxDepositAmount) + expect(supToken[3]).to.eq(defaultMaxDailyLimit) - const Factory__Teleportation = await ethers.getContractFactory( - 'Teleportation' + await Proxy__Teleportation.addSupportedToken( + L2Boba.address, + chainId4, + defaultMinDepositAmount, + defaultMaxDepositAmount, + defaultMaxDailyLimit + ) + supToken = await Proxy__Teleportation.supportedTokens( + L2Boba.address, + chainId4 ) - Teleportation = await Factory__Teleportation.deploy() - await Teleportation.deployTransaction.wait() - const Factory__Proxy__Teleportation = await ethers.getContractFactory( - 'Lib_ResolvedDelegateProxy' + expect(supToken[0]).to.eq(true) + expect(supToken[1]).to.eq(defaultMinDepositAmount) + expect(supToken[2]).to.eq(defaultMaxDepositAmount) + expect(supToken[3]).to.eq(defaultMaxDailyLimit) + + await Proxy__Teleportation.addSupportedToken( + RandomERC20.address, + chainId4, + defaultMinDepositAmount, + defaultMaxDepositAmount, + defaultMaxDailyLimit ) - Proxy__Teleportation = await Factory__Proxy__Teleportation.deploy( - Teleportation.address + supToken = await Proxy__Teleportation.supportedTokens( + RandomERC20.address, + chainId4 ) - await Proxy__Teleportation.deployTransaction.wait() - Proxy__Teleportation = new ethers.Contract( - Proxy__Teleportation.address, - Factory__Teleportation.interface, - signer + expect(supToken[0]).to.eq(true) + expect(supToken[1]).to.eq(defaultMinDepositAmount) + expect(supToken[2]).to.eq(defaultMaxDepositAmount) + expect(supToken[3]).to.eq(defaultMaxDailyLimit) + }) + + afterEach(async () => { + await Proxy__Teleportation.removeSupportedToken( + ethers.constants.AddressZero, + chainId4 + ) + let supToken = await Proxy__Teleportation.supportedTokens( + ethers.constants.AddressZero, + chainId4 ) - await Proxy__Teleportation.initialize( + expect(supToken[0]).to.eq(false) + expect(supToken[1]).to.eq('0') + expect(supToken[2]).to.eq('0') + expect(supToken[3]).to.eq('0') + + await Proxy__Teleportation.removeSupportedToken(L2Boba.address, chainId4) + supToken = await Proxy__Teleportation.supportedTokens( L2Boba.address, - ethers.utils.parseEther('1'), - ethers.utils.parseEther('100000') + chainId4 + ) + expect(supToken[0]).to.eq(false) + expect(supToken[1]).to.eq('0') + expect(supToken[2]).to.eq('0') + expect(supToken[3]).to.eq('0') + + await Proxy__Teleportation.removeSupportedToken( + RandomERC20.address, + chainId4 + ) + supToken = await Proxy__Teleportation.supportedTokens( + RandomERC20.address, + chainId4 ) + expect(supToken[0]).to.eq(false) + expect(supToken[1]).to.eq('0') + expect(supToken[2]).to.eq('0') + expect(supToken[3]).to.eq('0') }) - it('should revert when initialize again', async () => { - await expect( - Proxy__Teleportation.initialize( - L2Boba.address, - ethers.utils.parseEther('1'), - ethers.utils.parseEther('100000') // maxTransferAmountPerDay is set to 100000 + describe('Ethereum L2 - BOBA is not the native token', () => { + before(async () => { + signer = (await ethers.getSigners())[0] + signer2 = (await ethers.getSigners())[1] + signerAddress = await signer.getAddress() + signer2Address = await signer2.getAddress() + + L2Boba = await ( + await ethers.getContractFactory('L1ERC20') + ).deploy(initialSupply, tokenName, tokenSymbol, 18) + + RandomERC20 = await ( + await ethers.getContractFactory('L1ERC20') + ).deploy(initialSupply, 'Random Token', 'RTN', 9) + + const Factory__Teleportation = await ethers.getContractFactory( + 'Teleportation' ) - ).to.be.revertedWith('Contract has been initialized') - }) + Teleportation = await Factory__Teleportation.deploy() + await Teleportation.deployTransaction.wait() + const Factory__Proxy__Teleportation = await ethers.getContractFactory( + 'Lib_ResolvedDelegateProxy' + ) + Proxy__Teleportation = await Factory__Proxy__Teleportation.deploy( + Teleportation.address + ) + await Proxy__Teleportation.deployTransaction.wait() + Proxy__Teleportation = new ethers.Contract( + Proxy__Teleportation.address, + Factory__Teleportation.interface, + signer + ) + await Proxy__Teleportation.initialize() + }) - it('should add the supported chain', async () => { - await Proxy__Teleportation.addSupportedChain(4) - expect(await Proxy__Teleportation.supportedChains(4)).to.eq(true) - }) + it('should revert when initialize again', async () => { + await expect(Proxy__Teleportation.initialize()).to.be.revertedWith( + 'Contract has been initialized' + ) + }) - it('should not add the supported chain if it is added', async () => { - await expect( - Proxy__Teleportation.addSupportedChain(4) - ).to.be.revertedWith('Chain is already supported') - }) + it('should add supported token if it is zero address (equals native)', async () => { + await Proxy__Teleportation.removeSupportedToken( + ethers.constants.AddressZero, + chainId4 + ) - it('should not add the supported chain if caller is not owner', async () => { - await expect( - Proxy__Teleportation.connect(signer2).addSupportedChain(4) - ).to.be.revertedWith('Caller is not the owner') - }) + await Proxy__Teleportation.addSupportedToken( + ethers.constants.AddressZero, + chainId4, + defaultMinDepositAmount, + defaultMaxDepositAmount, + defaultMaxDailyLimit + ) + const supToken = await Proxy__Teleportation.supportedTokens( + ethers.constants.AddressZero, + chainId4 + ) - it('should remove the supported chain', async () => { - await Proxy__Teleportation.removeSupportedChain(4) - expect(await Proxy__Teleportation.supportedChains(4)).to.eq(false) - }) + expect(supToken[0]).to.eq(true) + expect(supToken[1]).to.eq(defaultMinDepositAmount) + expect(supToken[2]).to.eq(defaultMaxDepositAmount) - it('should not remove if it is already not supported', async () => { - await expect( - Proxy__Teleportation.removeSupportedChain(4) - ).to.be.revertedWith('Chain is already not supported') - }) + // keep as default + }) - it('should not remove the supported chain if caller is not owner', async () => { - await expect( - Proxy__Teleportation.connect(signer2).removeSupportedChain(4) - ).to.be.revertedWith('Caller is not the owner') - }) + it('should not add supported token if it is not a contract address', async () => { + await expect( + Proxy__Teleportation.addSupportedToken( + signerAddress, + chainId4, + defaultMinDepositAmount, + defaultMaxDepositAmount, + defaultMaxDailyLimit + ) + ).to.be.revertedWith('Not contract or native') + }) - it('should teleport BOBA tokens and emit event', async () => { - await Proxy__Teleportation.addSupportedChain(4) - const _amount = ethers.utils.parseEther('100') - const preBalance = await L2Boba.balanceOf(signerAddress) - await L2Boba.approve(Proxy__Teleportation.address, _amount) - await expect(Proxy__Teleportation.teleportBOBA(_amount, 4)) - .to.emit(Proxy__Teleportation, 'BobaReceived') - .withArgs(31337, 4, 0, signerAddress, _amount) - expect((await Proxy__Teleportation.totalDeposits(4)).toString()).to.be.eq( - '1' - ) - expect( - (await Proxy__Teleportation.transferredAmount()).toString() - ).to.be.eq(_amount.toString()) - const postBalance = await L2Boba.balanceOf(signerAddress) - expect(preBalance.sub(_amount)).to.be.eq(postBalance) - }) + it('should not add supported token if it is already added', async () => { + const addToken = await Proxy__Teleportation.supportedTokens( + L2Boba.address, + chainId4 + ) + expect(addToken[0]).to.eq(true) + + await expect( + Proxy__Teleportation.addSupportedToken( + L2Boba.address, + chainId4, + defaultMinDepositAmount, + defaultMaxDepositAmount, + defaultMaxDailyLimit + ) + ).to.be.revertedWith('Already supported') + }) - it('should not teleport BOBA tokens if the amount exceeds the daily limit', async () => { - const maxTransferAmountPerDay = - await Proxy__Teleportation.maxTransferAmountPerDay() - const transferredAmount = await Proxy__Teleportation.transferredAmount() - const _amount = maxTransferAmountPerDay.sub(transferredAmount).add(1) - await L2Boba.approve(Proxy__Teleportation.address, _amount) - await expect( - Proxy__Teleportation.teleportBOBA(_amount, 4) - ).to.be.revertedWith('max amount per day exceeded') - }) + it('should not add supported token if caller is not owner', async () => { + await expect( + Proxy__Teleportation.connect(signer2).addSupportedToken( + L2Boba.address, + chainId4, + defaultMinDepositAmount, + defaultMaxDepositAmount, + defaultMaxDailyLimit + ) + ).to.be.revertedWith('Caller is not the owner') + }) - it('should reset the transferred amount', async () => { - await ethers.provider.send('evm_increaseTime', [86400]) - const _amount = ethers.utils.parseEther('1') - const transferTimestampCheckPoint = - await Proxy__Teleportation.transferTimestampCheckPoint() - await L2Boba.approve(Proxy__Teleportation.address, _amount) - await Proxy__Teleportation.teleportBOBA(_amount, 4) - expect(await Proxy__Teleportation.transferredAmount()).to.be.eq(_amount) - expect( - await Proxy__Teleportation.transferTimestampCheckPoint() - ).to.be.not.eq(transferTimestampCheckPoint) - }) + it('should not remove token if it is already not supported', async () => { + await Proxy__Teleportation.removeSupportedToken( + L2Boba.address, + chainId4 + ) + await expect( + Proxy__Teleportation.removeSupportedToken(L2Boba.address, chainId4) + ).to.be.revertedWith('Already not supported') + await Proxy__Teleportation.addSupportedToken( + L2Boba.address, + chainId4, + defaultMinDepositAmount, + defaultMaxDepositAmount, + defaultMaxDailyLimit + ) + }) - it('should revert if call teleportNativeBOBA function', async () => { - const _amount = ethers.utils.parseEther('1') - await L2Boba.approve(Proxy__Teleportation.address, _amount) - await expect( - Proxy__Teleportation.teleportNativeBOBA(4) - ).to.be.revertedWith('Only alt L2s can call this function') - }) + it('should remove token if it is zero address', async () => { + await Proxy__Teleportation.removeSupportedToken( + ethers.constants.AddressZero, + chainId4 + ) - it('should revert if _toChainId is not supported', async () => { - const _amount = ethers.utils.parseEther('10') - await L2Boba.approve(Proxy__Teleportation.address, _amount) - await expect( - Proxy__Teleportation.teleportBOBA(_amount, 100) - ).to.be.revertedWith('Target chain is not supported') - }) + let supToken = await Proxy__Teleportation.supportedTokens( + ethers.constants.AddressZero, + chainId4 + ) + expect(supToken[0]).to.eq(false) + expect(supToken[1]).to.eq('0') + expect(supToken[2]).to.eq('0') + + await Proxy__Teleportation.addSupportedToken( + ethers.constants.AddressZero, + chainId4, + defaultMinDepositAmount, + defaultMaxDepositAmount, + defaultMaxDailyLimit + ) - it('should disburse BOBA tokens', async () => { - const preBalance = await L2Boba.balanceOf(Proxy__Teleportation.address) - const preSignerBalance = await L2Boba.balanceOf(signerAddress) - const payload = [ - { - amount: ethers.utils.parseEther('100'), - addr: signerAddress, - sourceChainId: 4, - depositId: 0, - }, - { - amount: ethers.utils.parseEther('1'), - addr: signerAddress, - sourceChainId: 4, - depositId: 1, - }, - ] - await L2Boba.approve( - Proxy__Teleportation.address, - ethers.utils.parseEther('101') - ) - await Proxy__Teleportation.disburseBOBA(payload) - const postBalance = await L2Boba.balanceOf(Proxy__Teleportation.address) - const postSignerBalance = await L2Boba.balanceOf(signerAddress) - expect( - (await Proxy__Teleportation.totalDisbursements(4)).toString() - ).to.be.eq('2') - expect(preBalance).to.be.eq(postBalance) - expect(postSignerBalance).to.be.eq(preSignerBalance) - }) + supToken = await Proxy__Teleportation.supportedTokens( + ethers.constants.AddressZero, + chainId4 + ) + // added by default + expect(supToken[0]).to.eq(true) + }) - it('should disburse BOBA tokens and emit events', async () => { - const _amount = ethers.utils.parseEther('100') - const payload = [ - { - amount: _amount, - addr: signerAddress, - sourceChainId: 4, - depositId: 2, - }, - ] - await L2Boba.approve(Proxy__Teleportation.address, _amount) - await expect(Proxy__Teleportation.disburseBOBA(payload)) - .to.emit(Proxy__Teleportation, 'DisbursementSuccess') - .withArgs(2, signerAddress, _amount, 4) - }) + it('should not remove the supported token if caller is not owner', async () => { + await expect( + Proxy__Teleportation.connect(signer2).removeSupportedToken( + L2Boba.address, + chainId4 + ) + ).to.be.revertedWith('Caller is not the owner') + }) - it('should not disburse BOBA tokens if the depositId is wrong', async () => { - const _amount = ethers.utils.parseEther('100') - const payload = [ - { - amount: _amount, - addr: signerAddress, - sourceChainId: 4, - depositId: 4, - }, - ] - await L2Boba.approve(Proxy__Teleportation.address, _amount) - await expect( - Proxy__Teleportation.disburseBOBA(payload) - ).to.be.revertedWith('Unexpected next deposit id') - }) + let depositId4 = 0 + it('should teleport BOBA tokens and emit event', async () => { + const _amount = ethers.utils.parseEther('200') + const preBalance = await L2Boba.balanceOf(signerAddress) + await L2Boba.approve(Proxy__Teleportation.address, _amount) + await expect( + Proxy__Teleportation.teleportAsset(L2Boba.address, _amount, chainId4) + ) + .to.emit(Proxy__Teleportation, 'AssetReceived') + .withArgs( + L2Boba.address, + chainId31337, + chainId4, + depositId4, + signerAddress, + _amount + ) + expect( + (await Proxy__Teleportation.totalDeposits(chainId4)).toString() + ).to.be.eq((++depositId4).toString()) + expect( + ( + await Proxy__Teleportation.supportedTokens(L2Boba.address, chainId4) + ).transferredAmount.toString() + ).to.be.eq(_amount.toString()) + const postBalance = await L2Boba.balanceOf(signerAddress) + expect(preBalance.sub(_amount)).to.be.eq(postBalance) + }) - it('should not disburse tokens if it is not approved', async () => { - const _amount = ethers.utils.parseEther('101') - const payload = [ - { - amount: _amount, - addr: signerAddress, - sourceChainId: 4, - depositId: 3, - }, - ] - await expect( - Proxy__Teleportation.disburseBOBA(payload) - ).to.be.revertedWith('ERC20: transfer amount exceeds allowance') - }) + it('should teleport random tokens and emit event', async () => { + const _amount = ethers.utils.parseEther('100') + const preBalance = await RandomERC20.balanceOf(signerAddress) + await RandomERC20.approve(Proxy__Teleportation.address, _amount) + await expect( + Proxy__Teleportation.teleportAsset( + RandomERC20.address, + _amount, + chainId4 + ) + ) + .to.emit(Proxy__Teleportation, 'AssetReceived') + .withArgs( + RandomERC20.address, + chainId31337, + chainId4, + depositId4, + signerAddress, + _amount + ) + expect( + (await Proxy__Teleportation.totalDeposits(chainId4)).toString() + ).to.be.eq((++depositId4).toString()) + expect( + ( + await Proxy__Teleportation.supportedTokens( + RandomERC20.address, + chainId4 + ) + ).transferredAmount.toString() + ).to.be.eq(ethers.utils.parseEther('100').toString()) + const postBalance = await RandomERC20.balanceOf(signerAddress) + expect(preBalance.sub(_amount)).to.be.eq(postBalance) + }) - it('should not disburse tokens if caller is not disburser', async () => { - const _amount = ethers.utils.parseEther('100') - await L2Boba.transfer(signer2Address, _amount) - const payload = [ - { - amount: _amount, - addr: signerAddress, - sourceChainId: 4, - depositId: 3, - }, - ] - await L2Boba.connect(signer2).approve( - Proxy__Teleportation.address, - _amount - ) - await expect( - Proxy__Teleportation.connect(signer2).disburseBOBA(payload) - ).to.be.revertedWith('Caller is not the disburser') - }) + it('should not teleport BOBA tokens if the amount exceeds the daily limit', async () => { + const _minAmount = ethers.utils.parseEther('0.11') + await Proxy__Teleportation.setMinAmount( + L2Boba.address, + chainId4, + _minAmount + ) + await L2Boba.approve(Proxy__Teleportation.address, _minAmount) + await Proxy__Teleportation.teleportAsset( + L2Boba.address, + _minAmount, + chainId4 + ) + const _amount = ethers.utils.parseEther('0.9') + await Proxy__Teleportation.setMaxAmount( + L2Boba.address, + chainId4, + _amount + ) + await Proxy__Teleportation.setMaxTransferAmountPerDay( + L2Boba.address, + chainId4, + ethers.utils.parseEther('1') + ) - it('should revert if disburse the native BOBA token', async () => { - const _amount = ethers.utils.parseEther('100') - const payload = [ - { - amount: _amount, - addr: signerAddress, - sourceChainId: 4, - depositId: 3, - }, - ] - await expect( - Proxy__Teleportation.disburseNativeBOBA(payload) - ).to.be.revertedWith('Only alt L2s can call this function') - }) + await L2Boba.approve(Proxy__Teleportation.address, _amount) + await expect( + Proxy__Teleportation.teleportAsset(L2Boba.address, _amount, chainId4) + ).to.be.revertedWith('max amount per day exceeded') - it('should transfer disburser to another wallet', async () => { - await Proxy__Teleportation.transferDisburser(signer2Address) - expect(await Proxy__Teleportation.disburser()).to.be.eq(signer2Address) - await Proxy__Teleportation.connect(signer).transferDisburser( - signerAddress - ) - }) + await Proxy__Teleportation.setMaxTransferAmountPerDay( + L2Boba.address, + chainId4, + defaultMaxDailyLimit + ) + await Proxy__Teleportation.setMaxAmount( + L2Boba.address, + chainId4, + defaultMaxDepositAmount + ) + await Proxy__Teleportation.setMinAmount( + L2Boba.address, + chainId4, + defaultMinDepositAmount + ) + }) - it('should not transfer disburser to another wallet if caller is not owner', async () => { - await expect( - Proxy__Teleportation.connect(signer2).transferDisburser(signer2Address) - ).to.be.revertedWith('Caller is not the owner') - }) + it('should reset the transferred amount', async () => { + await ethers.provider.send('evm_increaseTime', [86400]) + const _amount = ethers.utils.parseEther('1') + const transferTimestampCheckPoint = ( + await Proxy__Teleportation.supportedTokens(L2Boba.address, chainId4) + ).transferTimestampCheckPoint + await L2Boba.approve(Proxy__Teleportation.address, _amount) + await Proxy__Teleportation.teleportAsset( + L2Boba.address, + _amount, + chainId4 + ) + expect( + (await Proxy__Teleportation.supportedTokens(L2Boba.address, chainId4)) + .transferredAmount + ).to.be.eq(_amount) + expect( + (await Proxy__Teleportation.supportedTokens(L2Boba.address, chainId4)) + .transferTimestampCheckPoint + ).to.be.not.eq(transferTimestampCheckPoint) + }) - it('should withdraw BOBA balance', async () => { - const preSignerBalnce = await L2Boba.balanceOf(signerAddress) - const preBalance = await L2Boba.balanceOf(Proxy__Teleportation.address) - await Proxy__Teleportation.withdrawBOBABalance() - const postBalance = await L2Boba.balanceOf(Proxy__Teleportation.address) - const postSignerBalance = await L2Boba.balanceOf(signerAddress) - expect(preBalance.sub(postBalance)).to.be.eq( - postSignerBalance.sub(preSignerBalnce) - ) - expect(postBalance.toString()).to.be.eq('0') - }) + it('should revert if _toChainId is not supported', async () => { + const _amount = ethers.utils.parseEther('10') + await L2Boba.approve(Proxy__Teleportation.address, _amount) + await expect( + Proxy__Teleportation.teleportAsset(L2Boba.address, _amount, 100) + ).to.be.revertedWith('Token or chain not supported') + }) - it('should not withdraw BOBA balance if caller is not owner', async () => { - await expect( - Proxy__Teleportation.connect(signer2).withdrawBOBABalance() - ).to.be.revertedWith('Caller is not the owner') - }) + it('should disburse BOBA tokens', async () => { + const preBalance = await L2Boba.balanceOf(Proxy__Teleportation.address) + const preSignerBalance = await L2Boba.balanceOf(signerAddress) + const payload = [ + { + token: L2Boba.address, + amount: ethers.utils.parseEther('100'), + addr: signerAddress, + sourceChainId: chainId4, + depositId: 0, + }, + { + token: L2Boba.address, + amount: ethers.utils.parseEther('1'), + addr: signerAddress, + sourceChainId: chainId4, + depositId: 1, + }, + ] + await L2Boba.approve( + Proxy__Teleportation.address, + ethers.utils.parseEther('101') + ) + await Proxy__Teleportation.disburseAsset(payload) + const postBalance = await L2Boba.balanceOf(Proxy__Teleportation.address) + const postSignerBalance = await L2Boba.balanceOf(signerAddress) + expect( + (await Proxy__Teleportation.totalDisbursements(chainId4)).toString() + ).to.be.eq('2') + expect(preBalance).to.be.eq(postBalance) + expect(postSignerBalance).to.be.eq(preSignerBalance) + }) - it('should pause contract', async () => { - await Proxy__Teleportation.pause() - expect(await Proxy__Teleportation.paused()).to.be.eq(true) - await expect(Proxy__Teleportation.teleportBOBA(1, 4)).to.be.revertedWith( - 'Pausable: paused' - ) - await expect(Proxy__Teleportation.disburseBOBA([])).to.be.revertedWith( - 'Pausable: paused' - ) - }) + it('should disburse BOBA tokens and emit events', async () => { + const _amount = ethers.utils.parseEther('100') + const payload = [ + { + token: L2Boba.address, + amount: _amount, + addr: signerAddress, + sourceChainId: chainId4, + depositId: 2, + }, + ] + await L2Boba.approve(Proxy__Teleportation.address, _amount) + await expect(Proxy__Teleportation.disburseAsset(payload)) + .to.emit(Proxy__Teleportation, 'DisbursementSuccess') + .withArgs(2, signerAddress, L2Boba.address, _amount, chainId4) + }) - it('should unpause contract', async () => { - await Proxy__Teleportation.unpause() - expect(await Proxy__Teleportation.paused()).to.be.eq(false) - const _amount = ethers.utils.parseEther('100') - await L2Boba.approve(Proxy__Teleportation.address, _amount) - await expect(Proxy__Teleportation.teleportBOBA(_amount, 4)) - .to.emit(Proxy__Teleportation, 'BobaReceived') - .withArgs(31337, 4, 2, signerAddress, _amount) - expect((await Proxy__Teleportation.totalDeposits(4)).toString()).to.be.eq( - '3' - ) - const payload = [ - { - amount: _amount, - addr: signerAddress, - sourceChainId: 4, - depositId: 3, - }, - ] - await L2Boba.approve(Proxy__Teleportation.address, _amount) - await expect(Proxy__Teleportation.disburseBOBA(payload)) - .to.emit(Proxy__Teleportation, 'DisbursementSuccess') - .withArgs(3, signerAddress, _amount, 4) - }) - }) + it('should not disburse BOBA tokens if the depositId is wrong', async () => { + const _amount = ethers.utils.parseEther('100') + const payload = [ + { + token: L2Boba.address, + amount: _amount, + addr: signerAddress, + sourceChainId: chainId4, + depositId: 4, + }, + ] + await L2Boba.approve(Proxy__Teleportation.address, _amount) + await expect( + Proxy__Teleportation.disburseAsset(payload) + ).to.be.revertedWith('Unexpected next deposit id') + }) - describe('Alt L2 - BOBA is the native token', () => { - before(async () => { - signer = (await ethers.getSigners())[0] - signer2 = (await ethers.getSigners())[1] - signerAddress = await signer.getAddress() - signer2Address = await signer2.getAddress() + it('should not disburse BOBA tokens if disbursement is native/zero address', async () => { + const _amount = ethers.utils.parseEther('100') + const payload = [ + { + token: ethers.constants.AddressZero, + amount: _amount, + addr: signerAddress, + sourceChainId: chainId4, + depositId: 3, + }, + ] + await L2Boba.approve(Proxy__Teleportation.address, _amount) + await expect( + Proxy__Teleportation.disburseAsset(payload) + ).to.be.revertedWith('Disbursement total != amount sent') + }) - L2Boba = await ( - await ethers.getContractFactory('L1ERC20') - ).deploy(initialSupply, tokenName, tokenSymbol, 18) + it('should not disburse ERC20 tokens if any disbursement is native/zero address', async () => { + const _amount = ethers.utils.parseEther('100') + const payload = [ + { + token: L2Boba.address, + amount: _amount, + addr: signerAddress, + sourceChainId: chainId4, + depositId: 3, + }, + { + token: ethers.constants.AddressZero, + amount: _amount, + addr: signerAddress, + sourceChainId: chainId4, + depositId: 4, + }, + ] + await L2Boba.approve(Proxy__Teleportation.address, _amount) + await expect( + Proxy__Teleportation.disburseAsset(payload) + ).to.be.revertedWith('Disbursement total != amount sent') + }) - const Factory__Teleportation = await ethers.getContractFactory( - 'Teleportation' - ) - Teleportation = await Factory__Teleportation.deploy() - await Teleportation.deployTransaction.wait() - const Factory__Proxy__Teleportation = await ethers.getContractFactory( - 'Lib_ResolvedDelegateProxy' - ) - Proxy__Teleportation = await Factory__Proxy__Teleportation.deploy( - Teleportation.address - ) - await Proxy__Teleportation.deployTransaction.wait() - Proxy__Teleportation = new ethers.Contract( - Proxy__Teleportation.address, - Factory__Teleportation.interface, - signer - ) - await Proxy__Teleportation.initialize( - '0x4200000000000000000000000000000000000006', - ethers.utils.parseEther('1'), - ethers.utils.parseEther('100000') - ) - }) + it('should not disburse tokens if it is not approved', async () => { + const _amount = ethers.utils.parseEther('101') + const payload = [ + { + token: L2Boba.address, + amount: _amount, + addr: signerAddress, + sourceChainId: chainId4, + depositId: 3, + }, + ] + await expect( + Proxy__Teleportation.disburseAsset(payload) + ).to.be.revertedWith('ERC20: transfer amount exceeds allowance') + }) - it('should revert when initialize again', async () => { - await expect( - Proxy__Teleportation.initialize( - '0x4200000000000000000000000000000000000006', - ethers.utils.parseEther('1'), - ethers.utils.parseEther('100000') + it('should not disburse tokens if caller is not disburser', async () => { + const _amount = ethers.utils.parseEther('100') + await L2Boba.transfer(signer2Address, _amount) + const payload = [ + { + token: L2Boba.address, + amount: _amount, + addr: signerAddress, + sourceChainId: chainId4, + depositId: 3, + }, + ] + await L2Boba.connect(signer2).approve( + Proxy__Teleportation.address, + _amount ) - ).to.be.revertedWith('Contract has been initialized') - }) + await expect( + Proxy__Teleportation.connect(signer2).disburseAsset(payload) + ).to.be.revertedWith('Caller is not the disburser') + }) - it('should add the supported chain', async () => { - await Proxy__Teleportation.addSupportedChain(4) - expect(await Proxy__Teleportation.supportedChains(4)).to.eq(true) - }) + it('should transfer disburser to another wallet', async () => { + await Proxy__Teleportation.transferDisburser(signer2Address) + expect(await Proxy__Teleportation.disburser()).to.be.eq(signer2Address) + await Proxy__Teleportation.connect(signer).transferDisburser( + signerAddress + ) + }) - it('should not add the supported chain if it is added', async () => { - await expect( - Proxy__Teleportation.addSupportedChain(4) - ).to.be.revertedWith('Chain is already supported') - }) + it('should not transfer disburser to another wallet if caller is not owner', async () => { + await expect( + Proxy__Teleportation.connect(signer2).transferDisburser( + signer2Address + ) + ).to.be.revertedWith('Caller is not the owner') + }) - it('should not add the supported chain if caller is not owner', async () => { - await expect( - Proxy__Teleportation.connect(signer2).addSupportedChain(4) - ).to.be.revertedWith('Caller is not the owner') - }) + it('should withdraw ERC20 balance', async () => { + const preSignerBalnce = await L2Boba.balanceOf(signerAddress) + const preBalance = await L2Boba.balanceOf(Proxy__Teleportation.address) + await expect(Proxy__Teleportation.withdrawBalance(L2Boba.address)) + .to.emit(Proxy__Teleportation, 'AssetBalanceWithdrawn') + .withArgs(L2Boba.address, signerAddress, preBalance) + + const postBalance = await L2Boba.balanceOf(Proxy__Teleportation.address) + const postSignerBalance = await L2Boba.balanceOf(signerAddress) + expect(preBalance.sub(postBalance)).to.be.eq( + postSignerBalance.sub(preSignerBalnce) + ) + expect(postBalance.toString()).to.be.eq('0') + }) - it('should remove the supported chain', async () => { - await Proxy__Teleportation.removeSupportedChain(4) - expect(await Proxy__Teleportation.supportedChains(4)).to.eq(false) - }) + it('should not withdraw ERC20 balance if caller is not owner', async () => { + await expect( + Proxy__Teleportation.connect(signer2).withdrawBalance(L2Boba.address) + ).to.be.revertedWith('Caller is not the owner') + }) - it('should not remove if it is already not supported', async () => { - await expect( - Proxy__Teleportation.removeSupportedChain(4) - ).to.be.revertedWith('Chain is already not supported') - }) + it('should pause contract', async () => { + await Proxy__Teleportation.pause() + expect(await Proxy__Teleportation.paused()).to.be.eq(true) + await expect( + Proxy__Teleportation.teleportAsset(L2Boba.address, 1, chainId4) + ).to.be.revertedWith('Pausable: paused') + await expect(Proxy__Teleportation.disburseAsset([])).to.be.revertedWith( + 'Pausable: paused' + ) + }) - it('should not remove the supported chain if caller is not owner', async () => { - await expect( - Proxy__Teleportation.connect(signer2).removeSupportedChain(4) - ).to.be.revertedWith('Caller is not the owner') - }) + it('should unpause contract', async () => { + await Proxy__Teleportation.unpause() + expect(await Proxy__Teleportation.paused()).to.be.eq(false) + const _amount = ethers.utils.parseEther('100') + await L2Boba.approve(Proxy__Teleportation.address, _amount) + const depositId = 4 + await expect( + Proxy__Teleportation.teleportAsset(L2Boba.address, _amount, chainId4) + ) + .to.emit(Proxy__Teleportation, 'AssetReceived') + .withArgs( + L2Boba.address, + chainId31337, + chainId4, + depositId, + signerAddress, + _amount + ) + expect( + (await Proxy__Teleportation.totalDeposits(chainId4)).toString() + ).to.be.eq((depositId + 1).toString()) + const payload = [ + { + token: L2Boba.address, + amount: _amount, + addr: signerAddress, + sourceChainId: chainId4, + depositId: depositId - 1, + }, + ] + await L2Boba.approve(Proxy__Teleportation.address, _amount) + await expect(Proxy__Teleportation.disburseAsset(payload)) + .to.emit(Proxy__Teleportation, 'DisbursementSuccess') + .withArgs( + depositId - 1, + signerAddress, + L2Boba.address, + _amount, + chainId4 + ) + }) - it('should teleport BOBA tokens and emit event', async () => { - await Proxy__Teleportation.addSupportedChain(4) - const _amount = ethers.utils.parseEther('100') - const preBalance = await ethers.provider.getBalance(signerAddress) - await expect( - Proxy__Teleportation.teleportNativeBOBA(4, { value: _amount }) - ) - .to.emit(Proxy__Teleportation, 'BobaReceived') - .withArgs(31337, 4, 0, signerAddress, _amount) - expect((await Proxy__Teleportation.totalDeposits(4)).toString()).to.be.eq( - '1' - ) - expect( - (await Proxy__Teleportation.transferredAmount()).toString() - ).to.be.eq(_amount.toString()) + it('should teleport native asset and emit event', async () => { + const prevTransferredAmount = ( + await Proxy__Teleportation.supportedTokens( + ethers.constants.AddressZero, + chainId4 + ) + ).transferredAmount + const _amount = ethers.utils.parseEther('100') + const preBalance = await ethers.provider.getBalance(signerAddress) + const depositId = 5 + await expect( + Proxy__Teleportation.teleportAsset( + ethers.constants.AddressZero, + _amount, + chainId4, + { value: _amount } + ) + ) + .to.emit(Proxy__Teleportation, 'AssetReceived') + .withArgs( + ethers.constants.AddressZero, + chainId31337, + chainId4, + depositId, + signerAddress, + _amount + ) + expect( + (await Proxy__Teleportation.totalDeposits(chainId4)).toString() + ).to.be.eq((depositId + 1).toString()) + expect( + BigNumber.from( + ( + await Proxy__Teleportation.supportedTokens( + ethers.constants.AddressZero, + chainId4 + ) + ).transferredAmount + ) + .sub(prevTransferredAmount) + .toString() + ).to.be.eq(_amount.toString()) + + const gasFee = await getGasFeeFromLastestBlock(ethers.provider) + + const postBalance = await ethers.provider.getBalance(signerAddress) + expect(preBalance.sub(_amount)).to.be.eq(postBalance.add(gasFee)) + }) - const gasFee = await getGasFeeFromLastestBlock(ethers.provider) + it('should disburse native asset and emit events', async () => { + const _amount = ethers.utils.parseEther('100') + const payload = [ + { + token: ethers.constants.AddressZero, + amount: _amount, + addr: signerAddress, + sourceChainId: chainId4, + depositId: 4, + }, + ] + await expect( + Proxy__Teleportation.disburseAsset(payload, { value: _amount }) + ) + .to.emit(Proxy__Teleportation, 'DisbursementSuccess') + .withArgs( + 4, + signerAddress, + ethers.constants.AddressZero, + _amount, + chainId4 + ) + }) - const postBalance = await ethers.provider.getBalance(signerAddress) - expect(preBalance.sub(_amount)).to.be.eq(postBalance.add(gasFee)) - }) + it('should disburse native asset and token if disbursement cumulative value does match msg.value', async () => { + const tokenAmount = ethers.utils.parseEther('3') + await L2Boba.approve(Proxy__Teleportation.address, tokenAmount) + + const payload = [ + { + token: ethers.constants.AddressZero, + amount: ethers.utils.parseEther('10'), + addr: signerAddress, + sourceChainId: chainId4, + depositId: 5, + }, + { + token: L2Boba.address, + amount: tokenAmount, + addr: signerAddress, + sourceChainId: chainId4, + depositId: 6, + }, + { + token: ethers.constants.AddressZero, + amount: ethers.utils.parseEther('12'), + addr: signerAddress, + sourceChainId: chainId4, + depositId: 7, + }, + ] + await expect( + Proxy__Teleportation.disburseAsset(payload, { + value: ethers.utils.parseEther('22'), + }) + ) + .to.emit(Proxy__Teleportation, 'DisbursementSuccess') + .withArgs( + 5, + signerAddress, + ethers.constants.AddressZero, + ethers.utils.parseEther('10'), + chainId4 + ) + .to.emit(Proxy__Teleportation, 'DisbursementSuccess') + .withArgs(6, signerAddress, L2Boba.address, tokenAmount, chainId4) + .to.emit(Proxy__Teleportation, 'DisbursementSuccess') + .withArgs( + 7, + signerAddress, + ethers.constants.AddressZero, + ethers.utils.parseEther('12'), + chainId4 + ) + }) - it('should not teleport BOBA tokens if the amount exceeds the daily limit', async () => { - const maxTransferAmountPerDay = - await Proxy__Teleportation.maxTransferAmountPerDay() - const _amount = ethers.utils.parseEther('200') - await Proxy__Teleportation.setMaxTransferAmountPerDay(_amount) - await expect( - Proxy__Teleportation.teleportNativeBOBA(4, { - value: _amount, - }) - ).to.be.revertedWith('max amount per day exceeded') - await Proxy__Teleportation.setMaxTransferAmountPerDay( - maxTransferAmountPerDay - ) - }) + it('should not disburse native asset or token if disbursement cumulative value does not match msg.value', async () => { + const tokenAmount = ethers.utils.parseEther('3') + await L2Boba.approve(Proxy__Teleportation.address, tokenAmount) + + const payload = [ + { + token: ethers.constants.AddressZero, + amount: ethers.utils.parseEther('10'), + addr: signerAddress, + sourceChainId: chainId4, + depositId: 8, + }, + { + token: L2Boba.address, + amount: tokenAmount, + addr: signerAddress, + sourceChainId: chainId4, + depositId: 9, + }, + { + token: ethers.constants.AddressZero, + amount: ethers.utils.parseEther('12'), + addr: signerAddress, + sourceChainId: chainId4, + depositId: 10, + }, + ] + + await expect( + Proxy__Teleportation.disburseAsset(payload, { + value: ethers.utils.parseEther('21'), + }) + ).to.be.revertedWith('Disbursement total != amount sent') + }) - it('should reset the transferred amount', async () => { - await ethers.provider.send('evm_increaseTime', [86400]) - const _amount = ethers.utils.parseEther('1') - const transferTimestampCheckPoint = - await Proxy__Teleportation.transferTimestampCheckPoint() - await L2Boba.approve(Proxy__Teleportation.address, _amount) - await Proxy__Teleportation.teleportNativeBOBA(4, { - value: _amount, - }) - expect(await Proxy__Teleportation.transferredAmount()).to.be.eq(_amount) - expect( - await Proxy__Teleportation.transferTimestampCheckPoint() - ).to.be.not.eq(transferTimestampCheckPoint) - }) + it('should not disburse native asset or token if token is not supported on target network (or invalid address was passed -> e.g. faulty server-side mapping)', async () => { + const tokenAmount = ethers.utils.parseEther('3') + await L2Boba.approve(Proxy__Teleportation.address, tokenAmount) + + const payload = [ + { + token: ethers.constants.AddressZero, + amount: ethers.utils.parseEther('10'), + addr: signerAddress, + sourceChainId: chainId4, + depositId: 8, + }, + { + token: L2Boba.address, + amount: tokenAmount, + addr: signerAddress, + sourceChainId: chainId4, + depositId: 9, + }, + { + token: ethers.constants.AddressZero, + amount: ethers.utils.parseEther('12'), + addr: signerAddress, + sourceChainId: chainId4, + depositId: 10, + }, + ] + + await Proxy__Teleportation.removeSupportedToken( + L2Boba.address, + chainId4 + ) - it('should revert if call teleportBOBA function', async () => { - const _amount = ethers.utils.parseEther('1') - await expect( - Proxy__Teleportation.teleportBOBA(_amount, 4) - ).to.be.revertedWith('only non alt L2s') - }) + await expect( + Proxy__Teleportation.disburseAsset(payload, { + value: ethers.utils.parseEther('22'), + }) + ).to.be.revertedWith('Token or chain not supported') - it('should revert if _toChainId is not supported', async () => { - const _amount = ethers.utils.parseEther('10') - await expect( - Proxy__Teleportation.teleportNativeBOBA(100, { - value: _amount, - }) - ).to.be.revertedWith('Target chain is not supported') - }) + await Proxy__Teleportation.addSupportedToken( + L2Boba.address, + chainId4, + defaultMinDepositAmount, + defaultMaxDepositAmount, + defaultMaxDailyLimit + ) + }) - it('should disburse BOBA tokens', async () => { - const preBalance = await ethers.provider.getBalance( - Proxy__Teleportation.address - ) - const preSignerBalance = await ethers.provider.getBalance(signerAddress) - const payload = [ - { - amount: ethers.utils.parseEther('100'), - addr: signerAddress, - sourceChainId: 4, - depositId: 0, - }, - { - amount: ethers.utils.parseEther('1'), - addr: signerAddress, - sourceChainId: 4, - depositId: 1, - }, - ] - await Proxy__Teleportation.disburseNativeBOBA(payload, { - value: ethers.utils.parseEther('101'), - }) - const postBalance = await ethers.provider.getBalance( - Proxy__Teleportation.address - ) - const postSignerBalance = await ethers.provider.getBalance(signerAddress) - const gasFee = await getGasFeeFromLastestBlock(ethers.provider) - - expect( - (await Proxy__Teleportation.totalDisbursements(4)).toString() - ).to.be.eq('2') - expect(preBalance).to.be.eq(postBalance) - expect(postSignerBalance).to.be.eq(preSignerBalance.sub(gasFee)) - }) + it('should not disburse native asset or token if source chain is not supported on target network', async () => { + const tokenAmount = ethers.utils.parseEther('3') + await L2Boba.approve(Proxy__Teleportation.address, tokenAmount) + + const payload = [ + { + token: ethers.constants.AddressZero, + amount: ethers.utils.parseEther('10'), + addr: signerAddress, + sourceChainId: chainId4, + depositId: 8, + }, + { + token: L2Boba.address, + amount: tokenAmount, + addr: signerAddress, + sourceChainId: chainId4, + depositId: 9, + }, + { + token: ethers.constants.AddressZero, + amount: ethers.utils.parseEther('12'), + addr: signerAddress, + sourceChainId: chainId4, + depositId: 10, + }, + ] + + await Proxy__Teleportation.removeSupportedToken( + ethers.constants.AddressZero, + chainId4 + ) - it('should disburse BOBA tokens and emit events', async () => { - const _amount = ethers.utils.parseEther('100') - const payload = [ - { - amount: _amount, - addr: signerAddress, - sourceChainId: 4, - depositId: 2, - }, - ] - await expect( - Proxy__Teleportation.disburseNativeBOBA(payload, { value: _amount }) - ) - .to.emit(Proxy__Teleportation, 'DisbursementSuccess') - .withArgs(2, signerAddress, _amount, 4) + await expect( + Proxy__Teleportation.disburseAsset(payload, { + value: ethers.utils.parseEther('22'), + }) + ).to.be.revertedWith('Token or chain not supported') + + await Proxy__Teleportation.addSupportedToken( + ethers.constants.AddressZero, + chainId4, + defaultMinDepositAmount, + defaultMaxDepositAmount, + defaultMaxDailyLimit + ) + }) }) - it('should not disburse BOBA tokens if the depositId is wrong', async () => { - const _amount = ethers.utils.parseEther('100') - const payload = [ - { - amount: _amount, - addr: signerAddress, - sourceChainId: 4, - depositId: 4, - }, - ] - await expect( - Proxy__Teleportation.disburseNativeBOBA(payload, { value: _amount }) - ).to.be.revertedWith('Unexpected next deposit id') - }) + describe('Alt L2 - BOBA is the native token', () => { + before(async () => { + signer = (await ethers.getSigners())[0] + signer2 = (await ethers.getSigners())[1] + signerAddress = await signer.getAddress() + signer2Address = await signer2.getAddress() - it('should not disburse tokens if msg.value is wrong', async () => { - const _amount = ethers.utils.parseEther('101') - const payload = [ - { - amount: _amount, - addr: signerAddress, - sourceChainId: 4, - depositId: 3, - }, - ] - await expect( - Proxy__Teleportation.disburseNativeBOBA(payload, { value: '1' }) - ).to.be.revertedWith('Disbursement total != amount sent') - }) + L2Boba = await ( + await ethers.getContractFactory('L1ERC20') + ).deploy(initialSupply, tokenName, tokenSymbol, 18) + + const Factory__Teleportation = await ethers.getContractFactory( + 'Teleportation' + ) + Teleportation = await Factory__Teleportation.deploy() + await Teleportation.deployTransaction.wait() + const Factory__Proxy__Teleportation = await ethers.getContractFactory( + 'Lib_ResolvedDelegateProxy' + ) + Proxy__Teleportation = await Factory__Proxy__Teleportation.deploy( + Teleportation.address + ) + await Proxy__Teleportation.deployTransaction.wait() + Proxy__Teleportation = new ethers.Contract( + Proxy__Teleportation.address, + Factory__Teleportation.interface, + signer + ) + await Proxy__Teleportation.initialize() + }) - it('should not disburse tokens if caller is not disburser', async () => { - const _amount = ethers.utils.parseEther('100') - const payload = [ - { - amount: _amount, - addr: signerAddress, - sourceChainId: 4, - depositId: 3, - }, - ] - await expect( - Proxy__Teleportation.connect(signer2).disburseNativeBOBA(payload, { - value: _amount, + it('should revert when initialize again', async () => { + await expect(Proxy__Teleportation.initialize()).to.be.revertedWith( + 'Contract has been initialized' + ) + }) + + it('should teleport BOBA native tokens and emit event', async () => { + const _amount = ethers.utils.parseEther('100') + const preBalance = await ethers.provider.getBalance(signerAddress) + await expect( + Proxy__Teleportation.teleportAsset( + ethers.constants.AddressZero, + _amount, + chainId4, + { value: _amount } + ) + ) + .to.emit(Proxy__Teleportation, 'AssetReceived') + .withArgs( + ethers.constants.AddressZero, + chainId31337, + chainId4, + 0, + signerAddress, + _amount + ) + expect( + (await Proxy__Teleportation.totalDeposits(chainId4)).toString() + ).to.be.eq('1') + expect( + ( + await Proxy__Teleportation.supportedTokens( + ethers.constants.AddressZero, + chainId4 + ) + ).transferredAmount.toString() + ).to.be.eq(_amount.toString()) + + const gasFee = await getGasFeeFromLastestBlock(ethers.provider) + + const postBalance = await ethers.provider.getBalance(signerAddress) + expect(preBalance.sub(_amount)).to.be.eq(postBalance.add(gasFee)) + }) + + it('should not teleport native if the amount exceeds the daily limit', async () => { + const _minAmount = ethers.utils.parseEther('0.11') + await Proxy__Teleportation.setMinAmount( + ethers.constants.AddressZero, + chainId4, + _minAmount + ) + await L2Boba.approve(Proxy__Teleportation.address, _minAmount) + await Proxy__Teleportation.teleportAsset( + ethers.constants.AddressZero, + _minAmount, + chainId4, + {value: _minAmount} + ) + const _amount = ethers.utils.parseEther('0.9') + await Proxy__Teleportation.setMaxAmount( + ethers.constants.AddressZero, + chainId4, + _amount + ) + await Proxy__Teleportation.setMaxTransferAmountPerDay( + ethers.constants.AddressZero, + chainId4, + ethers.utils.parseEther('1') + ) + + await expect( + Proxy__Teleportation.teleportAsset( + ethers.constants.AddressZero, + _amount, + chainId4, + { value: _amount } + ) + ).to.be.revertedWith('max amount per day exceeded') + + await Proxy__Teleportation.setMaxTransferAmountPerDay( + L2Boba.address, + chainId4, + defaultMaxDailyLimit + ) + await Proxy__Teleportation.setMaxAmount( + L2Boba.address, + chainId4, + defaultMaxDepositAmount + ) + await Proxy__Teleportation.setMinAmount( + L2Boba.address, + chainId4, + defaultMinDepositAmount + ) + }) + + it('should reset the transferred amount', async () => { + await ethers.provider.send('evm_increaseTime', [86400]) + const _amount = ethers.utils.parseEther('1') + const transferTimestampCheckPoint = ( + await Proxy__Teleportation.supportedTokens( + ethers.constants.AddressZero, + chainId4 + ) + ).transferTimestampCheckPoint + await L2Boba.approve(Proxy__Teleportation.address, _amount) + await Proxy__Teleportation.teleportAsset( + ethers.constants.AddressZero, + _amount, + chainId4, + { + value: _amount, + } + ) + expect( + ( + await Proxy__Teleportation.supportedTokens( + ethers.constants.AddressZero, + chainId4 + ) + ).transferredAmount + ).to.be.eq(_amount) + expect( + ( + await Proxy__Teleportation.supportedTokens( + ethers.constants.AddressZero, + chainId4 + ) + ).transferTimestampCheckPoint + ).to.be.not.eq(transferTimestampCheckPoint) + }) + + it('should revert if _toChainId is not supported', async () => { + const _amount = ethers.utils.parseEther('10') + await expect( + Proxy__Teleportation.teleportAsset( + ethers.constants.AddressZero, + _amount, + 100, + { + value: _amount, + } + ) + ).to.be.revertedWith('Token or chain not supported') + }) + + it('should disburse BOBA tokens', async () => { + const preBalance = await ethers.provider.getBalance( + Proxy__Teleportation.address + ) + const preSignerBalance = await ethers.provider.getBalance(signerAddress) + const payload = [ + { + token: ethers.constants.AddressZero, + amount: ethers.utils.parseEther('100'), + addr: signerAddress, + sourceChainId: 4, + depositId: 0, + }, + { + token: ethers.constants.AddressZero, + amount: ethers.utils.parseEther('1'), + addr: signerAddress, + sourceChainId: 4, + depositId: 1, + }, + ] + await Proxy__Teleportation.disburseAsset(payload, { + value: ethers.utils.parseEther('101'), }) - ).to.be.revertedWith('Caller is not the disburser') - }) + const postBalance = await ethers.provider.getBalance( + Proxy__Teleportation.address + ) + const postSignerBalance = await ethers.provider.getBalance( + signerAddress + ) + const gasFee = await getGasFeeFromLastestBlock(ethers.provider) - it('should transfer disburser to another wallet', async () => { - await Proxy__Teleportation.transferDisburser(signer2Address) - expect(await Proxy__Teleportation.disburser()).to.be.eq(signer2Address) - await Proxy__Teleportation.connect(signer).transferDisburser( - signerAddress - ) - }) + expect( + (await Proxy__Teleportation.totalDisbursements(4)).toString() + ).to.be.eq('2') + expect(preBalance).to.be.eq(postBalance) + expect(postSignerBalance).to.be.eq(preSignerBalance.sub(gasFee)) + }) - it('should not transfer disburser to another wallet if caller is not owner', async () => { - await expect( - Proxy__Teleportation.connect(signer2).transferDisburser(signer2Address) - ).to.be.revertedWith('Caller is not the owner') - }) + it('should disburse BOBA tokens and emit events', async () => { + const _amount = ethers.utils.parseEther('100') + const payload = [ + { + token: ethers.constants.AddressZero, + amount: _amount, + addr: signerAddress, + sourceChainId: 4, + depositId: 2, + }, + ] + await expect( + Proxy__Teleportation.disburseAsset(payload, { value: _amount }) + ) + .to.emit(Proxy__Teleportation, 'DisbursementSuccess') + .withArgs(2, signerAddress, ethers.constants.AddressZero, _amount, 4) + }) - it('should withdraw BOBA balance', async () => { - const preSignerBalnce = await ethers.provider.getBalance(signerAddress) - const preBalance = await ethers.provider.getBalance( - Proxy__Teleportation.address - ) - await Proxy__Teleportation.withdrawNativeBOBABalance() - const postBalance = await ethers.provider.getBalance( - Proxy__Teleportation.address - ) - const postSignerBalance = await ethers.provider.getBalance(signerAddress) - const gasFee = await getGasFeeFromLastestBlock(ethers.provider) - expect(preBalance.sub(postBalance)).to.be.eq( - postSignerBalance.sub(preSignerBalnce).add(gasFee) - ) - expect(postBalance.toString()).to.be.eq('0') - }) + it('should not disburse BOBA tokens if the depositId is wrong', async () => { + const _amount = ethers.utils.parseEther('100') + const payload = [ + { + token: ethers.constants.AddressZero, + amount: _amount, + addr: signerAddress, + sourceChainId: 4, + depositId: 4, + }, + ] + await expect( + Proxy__Teleportation.disburseAsset(payload, { value: _amount }) + ).to.be.revertedWith('Unexpected next deposit id') + }) - it('should not withdraw BOBA balance if caller is not owner', async () => { - await expect( - Proxy__Teleportation.connect(signer2).withdrawNativeBOBABalance() - ).to.be.revertedWith('Caller is not the owner') - }) + it('should not disburse tokens if msg.value is wrong', async () => { + const _amount = ethers.utils.parseEther('101') + const payload = [ + { + token: ethers.constants.AddressZero, + amount: _amount, + addr: signerAddress, + sourceChainId: 4, + depositId: 3, + }, + ] + await expect( + Proxy__Teleportation.disburseAsset(payload, { value: '1' }) + ).to.be.revertedWith('Disbursement total != amount sent') + }) - it('should pause contract', async () => { - await Proxy__Teleportation.pause() - expect(await Proxy__Teleportation.paused()).to.be.eq(true) - await expect( - Proxy__Teleportation.teleportNativeBOBA(4, { value: 1 }) - ).to.be.revertedWith('Pausable: paused') - await expect( - Proxy__Teleportation.disburseNativeBOBA([]) - ).to.be.revertedWith('Pausable: paused') - }) + it('should not disburse tokens if caller is not disburser', async () => { + const _amount = ethers.utils.parseEther('100') + const payload = [ + { + token: ethers.constants.AddressZero, + amount: _amount, + addr: signerAddress, + sourceChainId: 4, + depositId: 3, + }, + ] + await expect( + Proxy__Teleportation.connect(signer2).disburseAsset(payload, { + value: _amount, + }) + ).to.be.revertedWith('Caller is not the disburser') + }) - it('should unpause contract', async () => { - await Proxy__Teleportation.unpause() - expect(await Proxy__Teleportation.paused()).to.be.eq(false) - const _amount = ethers.utils.parseEther('100') - await L2Boba.approve(Proxy__Teleportation.address, _amount) - await expect( - Proxy__Teleportation.teleportNativeBOBA(4, { value: _amount }) - ) - .to.emit(Proxy__Teleportation, 'BobaReceived') - .withArgs(31337, 4, 2, signerAddress, _amount) - expect((await Proxy__Teleportation.totalDeposits(4)).toString()).to.be.eq( - '3' - ) - const payload = [ - { - amount: _amount, - addr: signerAddress, - sourceChainId: 4, - depositId: 3, - }, - ] - await L2Boba.approve(Proxy__Teleportation.address, _amount) - await expect( - Proxy__Teleportation.disburseNativeBOBA(payload, { value: _amount }) - ) - .to.emit(Proxy__Teleportation, 'DisbursementSuccess') - .withArgs(3, signerAddress, _amount, 4) - }) - }) + it('should transfer disburser to another wallet', async () => { + await Proxy__Teleportation.transferDisburser(signer2Address) + expect(await Proxy__Teleportation.disburser()).to.be.eq(signer2Address) + await Proxy__Teleportation.connect(signer).transferDisburser( + signerAddress + ) + }) - describe('Alt L2 - Failed Disbursements', () => { - let PausedReceiver: Contract + it('should not transfer disburser to another wallet if caller is not owner', async () => { + await expect( + Proxy__Teleportation.connect(signer2).transferDisburser( + signer2Address + ) + ).to.be.revertedWith('Caller is not the owner') + }) - before(async () => { - signer = (await ethers.getSigners())[0] - signer2 = (await ethers.getSigners())[1] - signerAddress = await signer.getAddress() - signer2Address = await signer2.getAddress() + it('should withdraw BOBA balance', async () => { + const preSignerBalnce = await ethers.provider.getBalance(signerAddress) + const preBalance = await ethers.provider.getBalance( + Proxy__Teleportation.address + ) + await expect( + Proxy__Teleportation.withdrawBalance(ethers.constants.AddressZero) + ) + .to.emit(Proxy__Teleportation, 'AssetBalanceWithdrawn') + .withArgs(ethers.constants.AddressZero, signerAddress, preBalance) - L2Boba = await ( - await ethers.getContractFactory('L1ERC20') - ).deploy(initialSupply, tokenName, tokenSymbol, 18) + const postBalance = await ethers.provider.getBalance( + Proxy__Teleportation.address + ) + const postSignerBalance = await ethers.provider.getBalance( + signerAddress + ) + const gasFee = await getGasFeeFromLastestBlock(ethers.provider) + expect(preBalance.sub(postBalance)).to.be.eq( + postSignerBalance.sub(preSignerBalnce).add(gasFee) + ) + expect(postBalance.toString()).to.be.eq('0') + }) - const Factory__Teleportation = await ethers.getContractFactory( - 'Teleportation' - ) - Teleportation = await Factory__Teleportation.deploy() - await Teleportation.deployTransaction.wait() - const Factory__Proxy__Teleportation = await ethers.getContractFactory( - 'Lib_ResolvedDelegateProxy' - ) - Proxy__Teleportation = await Factory__Proxy__Teleportation.deploy( - Teleportation.address - ) - await Proxy__Teleportation.deployTransaction.wait() - Proxy__Teleportation = new ethers.Contract( - Proxy__Teleportation.address, - Factory__Teleportation.interface, - signer - ) - await Proxy__Teleportation.initialize( - '0x4200000000000000000000000000000000000006', - ethers.utils.parseEther('1'), - ethers.utils.parseEther('100000') - ) - }) + it('should not withdraw BOBA balance if caller is not owner', async () => { + await expect( + Proxy__Teleportation.connect(signer2).withdrawBalance( + ethers.constants.AddressZero + ) + ).to.be.revertedWith('Caller is not the owner') + }) - it('should emit events when disbursement of BOBA tokens fail', async () => { - await Proxy__Teleportation.addSupportedChain(4) - const Factory__PausedReceiver = await ethers.getContractFactory( - 'PausedReceiver' - ) - PausedReceiver = await Factory__PausedReceiver.deploy() - await PausedReceiver.setPauseStatus(true) - expect( - await ethers.provider.getBalance(Proxy__Teleportation.address) - ).to.be.eq(0) - const _amount = ethers.utils.parseEther('100') - const payload = [ - { - amount: _amount, - addr: PausedReceiver.address, //tweaking recipient address to an address that cannot receive native tokens - sourceChainId: 4, - depositId: 0, - }, - ] - await expect( - Proxy__Teleportation.disburseNativeBOBA(payload, { value: _amount }) - ) - .to.emit(Proxy__Teleportation, 'DisbursementFailed') - .withArgs(0, PausedReceiver.address, _amount, 4) - - const failedDisbursement = - await Proxy__Teleportation.failedNativeDisbursements(0) - expect(failedDisbursement.failed).to.be.eq(true) - expect(failedDisbursement.disbursement.amount).to.be.eq(_amount) - expect(failedDisbursement.disbursement.addr).to.be.eq( - PausedReceiver.address - ) - expect(failedDisbursement.disbursement.sourceChainId).to.be.eq(4) - expect(failedDisbursement.disbursement.depositId).to.be.eq(0) - expect( - await ethers.provider.getBalance(Proxy__Teleportation.address) - ).to.be.eq(_amount) - }) + it('should pause contract', async () => { + await Proxy__Teleportation.pause() + expect(await Proxy__Teleportation.paused()).to.be.eq(true) + await expect( + Proxy__Teleportation.teleportAsset( + ethers.constants.AddressZero, + 1, + 4, + { + value: 1, + } + ) + ).to.be.revertedWith('Pausable: paused') + await expect(Proxy__Teleportation.disburseAsset([])).to.be.revertedWith( + 'Pausable: paused' + ) + }) - it('should not be able to retry incorrect Disbursements', async () => { - await PausedReceiver.setPauseStatus(false) - await expect( - Proxy__Teleportation.retryDisburseNativeBOBA([1]) - ).to.be.revertedWith('DepositId is not a failed disbursement') - }) + it('should unpause contract', async () => { + await Proxy__Teleportation.unpause() + expect(await Proxy__Teleportation.paused()).to.be.eq(false) + const _amount = ethers.utils.parseEther('100') + await L2Boba.approve(Proxy__Teleportation.address, _amount) + await expect( + Proxy__Teleportation.teleportAsset( + ethers.constants.AddressZero, + _amount, + chainId4, + { value: _amount } + ) + ) + .to.emit(Proxy__Teleportation, 'AssetReceived') + .withArgs( + ethers.constants.AddressZero, + chainId31337, + chainId4, + 3, + signerAddress, + _amount + ) + expect( + (await Proxy__Teleportation.totalDeposits(chainId4)).toString() + ).to.be.eq('4') + const payload = [ + { + token: ethers.constants.AddressZero, + amount: _amount, + addr: signerAddress, + sourceChainId: 4, + depositId: 3, + }, + ] + await L2Boba.approve(Proxy__Teleportation.address, _amount) + await expect( + Proxy__Teleportation.disburseAsset(payload, { value: _amount }) + ) + .to.emit(Proxy__Teleportation, 'DisbursementSuccess') + .withArgs(3, signerAddress, ethers.constants.AddressZero, _amount, 4) + }) - it('should not be able to call from non disburser', async () => { - await PausedReceiver.setPauseStatus(false) - await expect( - Proxy__Teleportation.connect(signer2).retryDisburseNativeBOBA([0]) - ).to.be.revertedWith('Caller is not the disburser') - }) + it('should teleport BOBA tokens and emit event for alt l2', async () => { + const prevTransferredAmount = BigNumber.from( + (await Proxy__Teleportation.supportedTokens(L2Boba.address, chainId4)) + .transferredAmount + ) + const _amount = ethers.utils.parseEther('100') + const preBalance = await L2Boba.balanceOf(signerAddress) + await L2Boba.approve(Proxy__Teleportation.address, _amount) + await expect( + Proxy__Teleportation.teleportAsset(L2Boba.address, _amount, chainId4) + ) + .to.emit(Proxy__Teleportation, 'AssetReceived') + .withArgs(L2Boba.address, chainId31337, chainId4, 4, signerAddress, _amount) + expect( + (await Proxy__Teleportation.totalDeposits(chainId4)).toString() + ).to.be.eq('5') + expect( + BigNumber.from( + (await Proxy__Teleportation.supportedTokens(L2Boba.address, chainId4)) + .transferredAmount + ) + .sub(prevTransferredAmount) + .toString() + ).to.be.eq(_amount.toString()) + const postBalance = await L2Boba.balanceOf(signerAddress) + expect(preBalance.sub(_amount)).to.be.eq(postBalance) + }) - it('should be able to retry failed Disbursements', async () => { - const _amount = ethers.utils.parseEther('100') - await PausedReceiver.setPauseStatus(false) - await expect(Proxy__Teleportation.retryDisburseNativeBOBA([0])) - .to.emit(Proxy__Teleportation, 'DisbursementRetrySuccess') - .withArgs(0, PausedReceiver.address, _amount, 4) - - const failedDisbursement = - await Proxy__Teleportation.failedNativeDisbursements(0) - expect(failedDisbursement.failed).to.be.eq(false) - expect(failedDisbursement.disbursement.amount).to.be.eq(_amount) - expect(failedDisbursement.disbursement.addr).to.be.eq( - PausedReceiver.address - ) - expect(failedDisbursement.disbursement.sourceChainId).to.be.eq(4) - expect(failedDisbursement.disbursement.depositId).to.be.eq(0) - expect( - await ethers.provider.getBalance(Proxy__Teleportation.address) - ).to.be.eq(0) - expect(await ethers.provider.getBalance(PausedReceiver.address)).to.be.eq( - _amount - ) + it('should teleport random tokens and emit event for alt l2', async () => { + const prevTransferredAmount = BigNumber.from( + (await Proxy__Teleportation.supportedTokens(RandomERC20.address, chainId4)) + .transferredAmount + ) + const _amount = ethers.utils.parseEther('100') + const preBalance = await RandomERC20.balanceOf(signerAddress) + await RandomERC20.approve(Proxy__Teleportation.address, _amount) + await expect( + Proxy__Teleportation.teleportAsset(RandomERC20.address, _amount, chainId4) + ) + .to.emit(Proxy__Teleportation, 'AssetReceived') + .withArgs( + RandomERC20.address, + chainId31337, + chainId4, + 5, + signerAddress, + _amount + ) + expect( + (await Proxy__Teleportation.totalDeposits(4)).toString() + ).to.be.eq('6') + expect( + BigNumber.from( + (await Proxy__Teleportation.supportedTokens(RandomERC20.address, chainId4)) + .transferredAmount + ) + .sub(prevTransferredAmount) + .toString() + ).to.be.eq(_amount.toString()) + const postBalance = await RandomERC20.balanceOf(signerAddress) + expect(preBalance.sub(_amount)).to.be.eq(postBalance) + }) }) - it('should not be able to retry an already retried Disbursements', async () => { - await expect( - Proxy__Teleportation.retryDisburseNativeBOBA([0]) - ).to.be.revertedWith('DepositId is not a failed disbursement') - }) + describe('Alt L2 - Failed Disbursements', () => { + let PausedReceiver: Contract - it('should not be able to retry passed Disbursements', async () => { - const _amount = ethers.utils.parseEther('100') - const payload = [ - { - amount: _amount, - addr: signerAddress, - sourceChainId: 4, - depositId: 1, - }, - ] - await expect( - Proxy__Teleportation.disburseNativeBOBA(payload, { value: _amount }) - ) - .to.emit(Proxy__Teleportation, 'DisbursementSuccess') - .withArgs(1, signerAddress, _amount, 4) + before(async () => { + signer = (await ethers.getSigners())[0] + signer2 = (await ethers.getSigners())[1] + signerAddress = await signer.getAddress() + signer2Address = await signer2.getAddress() - await expect( - Proxy__Teleportation.retryDisburseNativeBOBA([1]) - ).to.be.revertedWith('DepositId is not a failed disbursement') - }) + L2Boba = await ( + await ethers.getContractFactory('L1ERC20') + ).deploy(initialSupply, tokenName, tokenSymbol, 18) - it('should be able to retry a batch of failed disbursements', async () => { - await PausedReceiver.setPauseStatus(true) - const _amount = ethers.utils.parseEther('100') - const payload = [ - { - amount: ethers.utils.parseEther('100'), - addr: PausedReceiver.address, - sourceChainId: 4, - depositId: 2, - }, - { - amount: ethers.utils.parseEther('1'), - addr: PausedReceiver.address, - sourceChainId: 4, - depositId: 3, - }, - ] - await expect( - Proxy__Teleportation.disburseNativeBOBA(payload, { - value: ethers.utils.parseEther('101'), - }) - ) - .to.emit(Proxy__Teleportation, 'DisbursementFailed') - .withArgs(2, PausedReceiver.address, _amount, 4) - - const failedDisbursement = - await Proxy__Teleportation.failedNativeDisbursements(2) - expect(failedDisbursement.failed).to.be.eq(true) - const failedDisbursement2 = - await Proxy__Teleportation.failedNativeDisbursements(3) - expect(failedDisbursement2.failed).to.be.eq(true) - expect( - await ethers.provider.getBalance(Proxy__Teleportation.address) - ).to.be.eq(ethers.utils.parseEther('101')) - - const preBalanceReceiver = await ethers.provider.getBalance( - PausedReceiver.address - ) - // retry disbursement - - await PausedReceiver.setPauseStatus(false) - await expect(Proxy__Teleportation.retryDisburseNativeBOBA([2, 3])) - .to.emit(Proxy__Teleportation, 'DisbursementRetrySuccess') - .withArgs(2, PausedReceiver.address, _amount, 4) - .to.emit(Proxy__Teleportation, 'DisbursementRetrySuccess') - .withArgs(3, PausedReceiver.address, ethers.utils.parseEther('1'), 4) - - const failedDisbursementRetried1 = - await Proxy__Teleportation.failedNativeDisbursements(2) - expect(failedDisbursementRetried1.failed).to.be.eq(false) - expect(failedDisbursementRetried1.disbursement.amount).to.be.eq(_amount) - expect(failedDisbursementRetried1.disbursement.addr).to.be.eq( - PausedReceiver.address - ) - expect(failedDisbursementRetried1.disbursement.sourceChainId).to.be.eq(4) - expect(failedDisbursementRetried1.disbursement.depositId).to.be.eq(2) - - const failedDisbursementRetried2 = - await Proxy__Teleportation.failedNativeDisbursements(3) - expect(failedDisbursementRetried2.failed).to.be.eq(false) - expect(failedDisbursementRetried2.disbursement.amount).to.be.eq( - ethers.utils.parseEther('1') - ) - expect(failedDisbursementRetried2.disbursement.addr).to.be.eq( - PausedReceiver.address - ) - expect(failedDisbursementRetried2.disbursement.sourceChainId).to.be.eq(4) - expect(failedDisbursementRetried2.disbursement.depositId).to.be.eq(3) - expect( - await ethers.provider.getBalance(Proxy__Teleportation.address) - ).to.be.eq(0) - expect(await ethers.provider.getBalance(PausedReceiver.address)).to.be.eq( - preBalanceReceiver.add(ethers.utils.parseEther('101')) - ) - }) - }) + const Factory__Teleportation = await ethers.getContractFactory( + 'Teleportation' + ) + Teleportation = await Factory__Teleportation.deploy() + await Teleportation.deployTransaction.wait() + const Factory__Proxy__Teleportation = await ethers.getContractFactory( + 'Lib_ResolvedDelegateProxy' + ) + Proxy__Teleportation = await Factory__Proxy__Teleportation.deploy( + Teleportation.address + ) + await Proxy__Teleportation.deployTransaction.wait() + Proxy__Teleportation = new ethers.Contract( + Proxy__Teleportation.address, + Factory__Teleportation.interface, + signer + ) + await Proxy__Teleportation.initialize() + }) - describe('Admin tests', () => { - before(async () => { - signer = (await ethers.getSigners())[0] - signer2 = (await ethers.getSigners())[1] - signerAddress = await signer.getAddress() - signer2Address = await signer2.getAddress() + it('should emit events when disbursement of BOBA tokens fail', async () => { + const Factory__PausedReceiver = await ethers.getContractFactory( + 'PausedReceiver' + ) + PausedReceiver = await Factory__PausedReceiver.deploy() + await PausedReceiver.setPauseStatus(true) + expect( + await ethers.provider.getBalance(Proxy__Teleportation.address) + ).to.be.eq(0) + const _amount = ethers.utils.parseEther('100') + const payload = [ + { + token: ethers.constants.AddressZero, + amount: _amount, + addr: PausedReceiver.address, //tweaking recipient address to an address that cannot receive native tokens + sourceChainId: 4, + depositId: 0, + }, + ] + await expect( + Proxy__Teleportation.disburseAsset(payload, { value: _amount }) + ) + .to.emit(Proxy__Teleportation, 'DisbursementFailed') + .withArgs(0, PausedReceiver.address, _amount, 4) + + const failedDisbursement = + await Proxy__Teleportation.failedNativeDisbursements(0) + expect(failedDisbursement.failed).to.be.eq(true) + expect(failedDisbursement.disbursement.amount).to.be.eq(_amount) + expect(failedDisbursement.disbursement.addr).to.be.eq( + PausedReceiver.address + ) + expect(failedDisbursement.disbursement.sourceChainId).to.be.eq(4) + expect(failedDisbursement.disbursement.depositId).to.be.eq(0) + expect( + await ethers.provider.getBalance(Proxy__Teleportation.address) + ).to.be.eq(_amount) + }) - L2Boba = await ( - await ethers.getContractFactory('L1ERC20') - ).deploy(initialSupply, tokenName, tokenSymbol, 18) + it('should not be able to retry incorrect Disbursements', async () => { + await PausedReceiver.setPauseStatus(false) + await expect( + Proxy__Teleportation.retryDisburseNative([1]) + ).to.be.revertedWith('DepositId not failed disbursement') + }) - const Factory__Teleportation = await ethers.getContractFactory( - 'Teleportation' - ) - Teleportation = await Factory__Teleportation.deploy() - await Teleportation.deployTransaction.wait() - const Factory__Proxy__Teleportation = await ethers.getContractFactory( - 'Lib_ResolvedDelegateProxy' - ) - Proxy__Teleportation = await Factory__Proxy__Teleportation.deploy( - Teleportation.address - ) - await Proxy__Teleportation.deployTransaction.wait() - Proxy__Teleportation = new ethers.Contract( - Proxy__Teleportation.address, - Factory__Teleportation.interface, - signer - ) - await Proxy__Teleportation.initialize( - L2Boba.address, - ethers.utils.parseEther('1'), - ethers.utils.parseEther('100000') - ) - }) + it('should not be able to call from non disburser', async () => { + await PausedReceiver.setPauseStatus(false) + await expect( + Proxy__Teleportation.connect(signer2).retryDisburseNative([0]) + ).to.be.revertedWith('Caller is not the disburser') + }) - it('should transferOwnership', async () => { - await Proxy__Teleportation.transferOwnership(signer2Address) - expect(await Proxy__Teleportation.owner()).to.be.eq(signer2Address) - await Proxy__Teleportation.connect(signer2).transferOwnership( - signerAddress - ) - }) + it('should be able to retry failed Disbursements', async () => { + const _amount = ethers.utils.parseEther('100') + await PausedReceiver.setPauseStatus(false) + await expect(Proxy__Teleportation.retryDisburseNative([0])) + .to.emit(Proxy__Teleportation, 'DisbursementRetrySuccess') + .withArgs(0, PausedReceiver.address, _amount, 4) + + const failedDisbursement = + await Proxy__Teleportation.failedNativeDisbursements(0) + expect(failedDisbursement.failed).to.be.eq(false) + expect(failedDisbursement.disbursement.amount).to.be.eq(_amount) + expect(failedDisbursement.disbursement.addr).to.be.eq( + PausedReceiver.address + ) + expect(failedDisbursement.disbursement.sourceChainId).to.be.eq(4) + expect(failedDisbursement.disbursement.depositId).to.be.eq(0) + expect( + await ethers.provider.getBalance(Proxy__Teleportation.address) + ).to.be.eq(0) + expect( + await ethers.provider.getBalance(PausedReceiver.address) + ).to.be.eq(_amount) + }) - it('should not transferOwnership if caller is not owner', async () => { - await expect( - Proxy__Teleportation.connect(signer2).transferOwnership(signer2Address) - ).to.be.revertedWith('Caller is not the owner') - }) + it('should not be able to retry an already retried Disbursements', async () => { + await expect( + Proxy__Teleportation.retryDisburseNative([0]) + ).to.be.revertedWith('DepositId not failed disbursement') + }) - it('should not set minimum amount if caller is not owner', async () => { - const maxAmount = await Proxy__Teleportation.maxDepositAmount() - await expect( - Proxy__Teleportation.setMinAmount(maxAmount.add(1)) - ).to.be.revertedWith('incorrect min deposit amount') - }) + it('should not be able to retry passed Disbursements', async () => { + const _amount = ethers.utils.parseEther('100') + const payload = [ + { + token: ethers.constants.AddressZero, + amount: _amount, + addr: signerAddress, + sourceChainId: 4, + depositId: 1, + }, + ] + await expect( + Proxy__Teleportation.disburseAsset(payload, { value: _amount }) + ) + .to.emit(Proxy__Teleportation, 'DisbursementSuccess') + .withArgs(1, signerAddress, ethers.constants.AddressZero, _amount, 4) - it('should set minimum amount', async () => { - await Proxy__Teleportation.setMinAmount(ethers.utils.parseEther('1')) - expect( - (await Proxy__Teleportation.minDepositAmount()).toString() - ).to.be.eq(ethers.utils.parseEther('1')) - }) + await expect( + Proxy__Teleportation.retryDisburseNative([1]) + ).to.be.revertedWith('DepositId not failed disbursement') + }) - it('should not set minimum amount if caller is not owner', async () => { - await expect( - Proxy__Teleportation.connect(signer2).setMinAmount( + it('should be able to retry a batch of failed disbursements', async () => { + await PausedReceiver.setPauseStatus(true) + const _amount = ethers.utils.parseEther('100') + const payload = [ + { + token: ethers.constants.AddressZero, + amount: ethers.utils.parseEther('100'), + addr: PausedReceiver.address, + sourceChainId: 4, + depositId: 2, + }, + { + token: ethers.constants.AddressZero, + amount: ethers.utils.parseEther('1'), + addr: PausedReceiver.address, + sourceChainId: 4, + depositId: 3, + }, + ] + await expect( + Proxy__Teleportation.disburseAsset(payload, { + value: ethers.utils.parseEther('101'), + }) + ) + .to.emit(Proxy__Teleportation, 'DisbursementFailed') + .withArgs(2, PausedReceiver.address, _amount, 4) + + const failedDisbursement = + await Proxy__Teleportation.failedNativeDisbursements(2) + expect(failedDisbursement.failed).to.be.eq(true) + const failedDisbursement2 = + await Proxy__Teleportation.failedNativeDisbursements(3) + expect(failedDisbursement2.failed).to.be.eq(true) + expect( + await ethers.provider.getBalance(Proxy__Teleportation.address) + ).to.be.eq(ethers.utils.parseEther('101')) + + const preBalanceReceiver = await ethers.provider.getBalance( + PausedReceiver.address + ) + // retry disbursement + + await PausedReceiver.setPauseStatus(false) + await expect(Proxy__Teleportation.retryDisburseNative([2, 3])) + .to.emit(Proxy__Teleportation, 'DisbursementRetrySuccess') + .withArgs(2, PausedReceiver.address, _amount, 4) + .to.emit(Proxy__Teleportation, 'DisbursementRetrySuccess') + .withArgs(3, PausedReceiver.address, ethers.utils.parseEther('1'), 4) + + const failedDisbursementRetried1 = + await Proxy__Teleportation.failedNativeDisbursements(2) + expect(failedDisbursementRetried1.failed).to.be.eq(false) + expect(failedDisbursementRetried1.disbursement.amount).to.be.eq(_amount) + expect(failedDisbursementRetried1.disbursement.addr).to.be.eq( + PausedReceiver.address + ) + expect(failedDisbursementRetried1.disbursement.sourceChainId).to.be.eq( + 4 + ) + expect(failedDisbursementRetried1.disbursement.depositId).to.be.eq(2) + + const failedDisbursementRetried2 = + await Proxy__Teleportation.failedNativeDisbursements(3) + expect(failedDisbursementRetried2.failed).to.be.eq(false) + expect(failedDisbursementRetried2.disbursement.amount).to.be.eq( ethers.utils.parseEther('1') ) - ).to.be.revertedWith('Caller is not the owner') + expect(failedDisbursementRetried2.disbursement.addr).to.be.eq( + PausedReceiver.address + ) + expect(failedDisbursementRetried2.disbursement.sourceChainId).to.be.eq( + 4 + ) + expect(failedDisbursementRetried2.disbursement.depositId).to.be.eq(3) + expect( + await ethers.provider.getBalance(Proxy__Teleportation.address) + ).to.be.eq(0) + expect( + await ethers.provider.getBalance(PausedReceiver.address) + ).to.be.eq(preBalanceReceiver.add(ethers.utils.parseEther('101'))) + }) }) - it('should not set maximum amount if caller is not owner', async () => { - const minAmount = await Proxy__Teleportation.minDepositAmount() - await expect( - Proxy__Teleportation.setMaxAmount(minAmount.sub(1)) - ).to.be.revertedWith('incorrect max deposit amount') - }) + describe('Admin tests', () => { + before(async () => { + signer = (await ethers.getSigners())[0] + signer2 = (await ethers.getSigners())[1] + signerAddress = await signer.getAddress() + signer2Address = await signer2.getAddress() - it('should set maximum amount', async () => { - await Proxy__Teleportation.setMaxAmount(ethers.utils.parseEther('1')) - expect( - (await Proxy__Teleportation.maxDepositAmount()).toString() - ).to.be.eq(ethers.utils.parseEther('1')) - }) + L2Boba = await ( + await ethers.getContractFactory('L1ERC20') + ).deploy(initialSupply, tokenName, tokenSymbol, 18) - it('should not set maximum amount if caller is not owner', async () => { - await expect( - Proxy__Teleportation.connect(signer2).setMaxAmount( - ethers.utils.parseEther('1') + const Factory__Teleportation = await ethers.getContractFactory( + 'Teleportation' ) - ).to.be.revertedWith('Caller is not the owner') - }) + Teleportation = await Factory__Teleportation.deploy() + await Teleportation.deployTransaction.wait() + const Factory__Proxy__Teleportation = await ethers.getContractFactory( + 'Lib_ResolvedDelegateProxy' + ) + Proxy__Teleportation = await Factory__Proxy__Teleportation.deploy( + Teleportation.address + ) + await Proxy__Teleportation.deployTransaction.wait() + Proxy__Teleportation = new ethers.Contract( + Proxy__Teleportation.address, + Factory__Teleportation.interface, + signer + ) + await Proxy__Teleportation.initialize() + }) - it('should set daily limit', async () => { - await Proxy__Teleportation.setMaxTransferAmountPerDay( - ethers.utils.parseEther('1') - ) - expect( - (await Proxy__Teleportation.maxTransferAmountPerDay()).toString() - ).to.be.eq(ethers.utils.parseEther('1')) - }) + it('should transferOwnership', async () => { + await Proxy__Teleportation.transferOwnership(signer2Address) + expect(await Proxy__Teleportation.owner()).to.be.eq(signer2Address) + await Proxy__Teleportation.connect(signer2).transferOwnership( + signerAddress + ) + }) + + it('should not transferOwnership if caller is not owner', async () => { + await expect( + Proxy__Teleportation.connect(signer2).transferOwnership(signer2Address) + ).to.be.revertedWith('Caller is not the owner') + }) + + it('should not set minimum amount if larger than maximum amount', async () => { + const maxAmount = BigNumber.from( + ( + await Proxy__Teleportation.supportedTokens( + ethers.constants.AddressZero, + chainId4 + ) + ).maxDepositAmount + ) + + await expect( + Proxy__Teleportation.setMinAmount( + ethers.constants.AddressZero, + chainId4, + maxAmount.add(1) + ) + ).to.be.revertedWith('incorrect min deposit amount') + }) + + it('should set minimum amount', async () => { + + await Proxy__Teleportation.setMinAmount( + L2Boba.address, + chainId4, + defaultMinDepositAmount.add(1) + ) + expect( + ( + await Proxy__Teleportation.supportedTokens(L2Boba.address, chainId4) + ).minDepositAmount.toString() + ).to.be.eq(defaultMinDepositAmount.add(1)) + + await Proxy__Teleportation.setMinAmount( + L2Boba.address, + chainId4, + defaultMinDepositAmount + ) + }) - it('should not set daily limit if caller is not owner', async () => { - await expect( - Proxy__Teleportation.connect(signer2).setMaxTransferAmountPerDay( + it('should set minimum amount native', async () => { + + await Proxy__Teleportation.setMinAmount( + ethers.constants.AddressZero, + chainId4, ethers.utils.parseEther('1') ) - ).to.be.revertedWith('Caller is not the owner') - }) - }) + expect( + ( + await Proxy__Teleportation.supportedTokens( + ethers.constants.AddressZero, chainId4 + ) + ).minDepositAmount.toString() + ).to.be.eq(ethers.utils.parseEther('1')) + }) + + it('should not set minimum amount if caller is not owner', async () => { + await expect( + Proxy__Teleportation.connect(signer2).setMinAmount( + L2Boba.address, chainId4, + ethers.utils.parseEther('1') + ) + ).to.be.revertedWith('Caller is not the owner') + }) - describe('Init tests', () => { - before(async () => { - signer = (await ethers.getSigners())[0] - signer2 = (await ethers.getSigners())[1] - signerAddress = await signer.getAddress() - signer2Address = await signer2.getAddress() + it('should not set maximum amount if smaller than min amount', async () => { - L2Boba = await ( - await ethers.getContractFactory('L1ERC20') - ).deploy(initialSupply, tokenName, tokenSymbol, 18) + await expect( + Proxy__Teleportation.setMaxAmount( + ethers.constants.AddressZero, chainId4, + defaultMinDepositAmount.sub(1) + ) + ).to.be.revertedWith('incorrect max deposit amount') + }) - const Factory__Teleportation = await ethers.getContractFactory( - 'Teleportation' - ) - Teleportation = await Factory__Teleportation.deploy() - await Teleportation.deployTransaction.wait() - const Factory__Proxy__Teleportation = await ethers.getContractFactory( - 'Lib_ResolvedDelegateProxy' - ) - Proxy__Teleportation = await Factory__Proxy__Teleportation.deploy( - Teleportation.address - ) - await Proxy__Teleportation.deployTransaction.wait() - Proxy__Teleportation = new ethers.Contract( - Proxy__Teleportation.address, - Factory__Teleportation.interface, - signer - ) - }) + it('should set maximum amount', async () => { + await Proxy__Teleportation.setMaxAmount( + ethers.constants.AddressZero, + chainId4, + defaultMaxDepositAmount.sub(2) + ) + expect( + ( + await Proxy__Teleportation.supportedTokens( + ethers.constants.AddressZero, chainId4 + ) + ).maxDepositAmount.toString() + ).to.be.eq(defaultMaxDepositAmount.sub(2)) + + await Proxy__Teleportation.setMaxAmount( + ethers.constants.AddressZero, + chainId4, + defaultMaxDepositAmount + ) + }) + + it('should set maximum amount native', async () => { + await Proxy__Teleportation.setMaxAmount( + ethers.constants.AddressZero, + chainId4, + defaultMaxDepositAmount.sub(2) + ) + expect( + ( + await Proxy__Teleportation.supportedTokens( + ethers.constants.AddressZero, chainId4 + ) + ).maxDepositAmount.toString() + ).to.be.eq(defaultMaxDepositAmount.sub(2)) + + await Proxy__Teleportation.setMaxAmount( + ethers.constants.AddressZero, + chainId4, + defaultMaxDepositAmount + ) + }) - it('should not be able to set incorrect init values', async () => { - await expect( - Proxy__Teleportation.initialize( + it('should not set maximum amount if caller is not owner', async () => { + await expect( + Proxy__Teleportation.connect(signer2).setMaxAmount( + L2Boba.address, + chainId4, + ethers.utils.parseEther('1') + ) + ).to.be.revertedWith('Caller is not the owner') + }) + + it('should set daily limit', async () => { + await Proxy__Teleportation.setMaxTransferAmountPerDay( L2Boba.address, - ethers.utils.parseEther('1'), - ethers.utils.parseEther('10000000000') + chainId4, + defaultMaxDailyLimit.add(2) ) - ).to.be.revertedWith('max deposit amount cannot be more than daily limit') + expect( + ( + await Proxy__Teleportation.supportedTokens(L2Boba.address, chainId4) + ).maxTransferAmountPerDay.toString() + ).to.be.eq(defaultMaxDailyLimit.add(2)) + }) + + it('should set daily limit native', async () => { + await Proxy__Teleportation.setMaxTransferAmountPerDay( + ethers.constants.AddressZero, + chainId4, + defaultMaxDailyLimit.add(2) + ) + expect( + ( + await Proxy__Teleportation.supportedTokens( + ethers.constants.AddressZero, + chainId4 + ) + ).maxTransferAmountPerDay.toString() + ).to.be.eq(defaultMaxDailyLimit.add(2)) + }) + + it('should not set daily limit if caller is not owner', async () => { + await expect( + Proxy__Teleportation.connect(signer2).setMaxTransferAmountPerDay( + ethers.constants.AddressZero, + chainId4, + ethers.utils.parseEther('1') + ) + ).to.be.revertedWith('Caller is not the owner') + }) }) }) }) diff --git a/packages/boba/teleportation/README.md b/packages/boba/teleportation/README.md index 86431988c6..c531496391 100644 --- a/packages/boba/teleportation/README.md +++ b/packages/boba/teleportation/README.md @@ -28,3 +28,28 @@ All configuration is done via environment variables. See all variables at [.env. Connect to Postgres on CLI: `psql --username postgres -d postgres --password` + + +## Deployments + +Audits outstanding. + +--- + +## Testnet deployments + +### Goerli (L1) +- Teleportation deployed to: `0x0D42E13a3a7203C281Ca72e90AF992781259197C` +- Proxy__Teleportation deployed to: `0x9A597f96899d9cc7Ba0Bd8a4148d7B7Ed6AA0300` + +### Boba Goerli +- Teleportation deployed to: `0x3Ad2babB5E8E4f7a5cAc75d330655ab6f0FBa14A` +- Proxy__Teleportation deployed to: `0x2af1C32E1dE8e041B7E45525A1Ca3C519Fac312F` + +### Boba Avax Testnet +- Teleportation deployed to: `0xB5FFFbB049DA94611b488f0921735b4B07e0BDDE` +- Proxy__Teleportation deployed to: `0x9A57d90E80BE60340f804fd2D0373dd34AB934A2` + +### Boba BNB Testnet +- Teleportation deployed to: `0xf7dE3869B7a0333e6B3a513A37Dc6270041BCC05` +- Proxy__Teleportation deployed to: `0xE3B5FB4CDa3C4c58A804e8856B5eC81D87972512` diff --git a/packages/boba/teleportation/package.json b/packages/boba/teleportation/package.json index 8a3fc5a5d1..4211b5c1ab 100644 --- a/packages/boba/teleportation/package.json +++ b/packages/boba/teleportation/package.json @@ -7,7 +7,7 @@ "test/**/*.ts" ], "scripts": { - "start": "ts-node src/index.ts", + "start": "ts-node ./src/exec/run.ts", "build": "tsc -p ./tsconfig.json", "clean": "rimraf dist/ ./tsconfig.tsbuildinfo", "lint": "yarn lint:fix && yarn lint:check", diff --git a/packages/boba/teleportation/src/data-source.ts b/packages/boba/teleportation/src/data-source.ts index 6db5b6dca2..02d529e66d 100644 --- a/packages/boba/teleportation/src/data-source.ts +++ b/packages/boba/teleportation/src/data-source.ts @@ -1,6 +1,6 @@ import 'reflect-metadata' import { DataSource } from 'typeorm' -import { HistoryData } from './entity/HistoryData' +import { HistoryData } from './entities/HistoryData.entity' import * as postgres from 'pg' // keep depcheck (db driver) import dotenv from 'dotenv' diff --git a/packages/boba/teleportation/src/entity/HistoryData.ts b/packages/boba/teleportation/src/entities/HistoryData.entity.ts similarity index 59% rename from packages/boba/teleportation/src/entity/HistoryData.ts rename to packages/boba/teleportation/src/entities/HistoryData.entity.ts index 05e214d727..7fafd27a79 100644 --- a/packages/boba/teleportation/src/entity/HistoryData.ts +++ b/packages/boba/teleportation/src/entities/HistoryData.entity.ts @@ -1,11 +1,11 @@ import { Entity, Column } from 'typeorm' import { PrimaryColumn } from 'typeorm/decorator/columns/PrimaryColumn' -@Entity() +@Entity({ name: 'history_data' }) export class HistoryData { - @PrimaryColumn({ type: 'int' }) + @PrimaryColumn({ type: 'int', name: 'chain_id' }) chainId: string | number - @Column({ type: 'int' }) + @Column({ type: 'int', name: 'block_no' }) blockNo: number } diff --git a/packages/boba/teleportation/src/exec/run.ts b/packages/boba/teleportation/src/exec/run.ts index 3a26cd4710..cdcf664a29 100644 --- a/packages/boba/teleportation/src/exec/run.ts +++ b/packages/boba/teleportation/src/exec/run.ts @@ -10,15 +10,25 @@ import { TeleportationService } from '../service' import { BobaChains } from '../utils/chains' /* Imports: Interface */ -import { ChainInfo } from '../utils/types' +import { ChainInfo, SupportedAssets } from '../utils/types' import { AppDataSource } from '../data-source' +import { HistoryData } from '../entities/HistoryData.entity' +import { Init1687802800701 } from '../migrations/1687802800701-00_Init' dotenv.config() const main = async () => { if (!AppDataSource.isInitialized) { + AppDataSource.setOptions({ + migrationsRun: true, + logging: false, + synchronize: false, + entities: [HistoryData], + migrations: [Init1687802800701], + }) await AppDataSource.initialize() // initialize DB connection } + console.log('Database initialized: ', AppDataSource.isInitialized) const config: Bcfg = new Config('teleportation') config.load({ @@ -58,27 +68,31 @@ const main = async () => { // get all boba chains and exclude the current chain const chainId = (await l2Provider.getNetwork()).chainId const isTestnet = BobaChains[chainId].testnet + let originSupportedAssets: SupportedAssets const selectedBobaChains: ChainInfo[] = Object.keys(BobaChains).reduce( (acc, cur) => { const chain = BobaChains[cur] - if (isTestnet === chain.testnet && Number(cur) !== chainId) { - chain.provider = new providers.StaticJsonRpcProvider(chain.url) - acc.push({ chainId: cur, ...chain }) + if (isTestnet === chain.testnet) { + if (Number(cur) !== chainId) { + chain.provider = new providers.StaticJsonRpcProvider(chain.url) + acc.push({ chainId: cur, ...chain }) + } else { + originSupportedAssets = chain.supportedAssets + } } return acc }, [] ) - const BOBA_TOKEN_ADDRESS = BobaChains[chainId].BobaTokenAddress const TELEPORTATION_ADDRESS = BobaChains[chainId].teleportationAddress const service = new TeleportationService({ l2RpcProvider: l2Provider, chainId, teleportationAddress: TELEPORTATION_ADDRESS, - bobaTokenAddress: BOBA_TOKEN_ADDRESS, disburserWallet, selectedBobaChains, + ownSupportedAssets: originSupportedAssets, pollingInterval: POLLING_INTERVAL, blockRangePerPolling: BLOCK_RANGE_PER_POLLING, }) diff --git a/packages/boba/teleportation/src/migrations/1687802800701-00_Init.ts b/packages/boba/teleportation/src/migrations/1687802800701-00_Init.ts new file mode 100644 index 0000000000..aaf3db27c2 --- /dev/null +++ b/packages/boba/teleportation/src/migrations/1687802800701-00_Init.ts @@ -0,0 +1,11 @@ +import {MigrationInterface, QueryRunner, Table} from 'typeorm' + +export class Init1687802800701 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`CREATE TABLE IF NOT EXISTS history_data (chain_id int NOT NULL, block_no int NULL, PRIMARY KEY (chain_id))`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.dropTable('history_data', true) + } +} diff --git a/packages/boba/teleportation/src/service.ts b/packages/boba/teleportation/src/service.ts index 0efaa54d25..bc5b409dfb 100644 --- a/packages/boba/teleportation/src/service.ts +++ b/packages/boba/teleportation/src/service.ts @@ -1,5 +1,12 @@ /* Imports: External */ -import { Contract, Wallet, BigNumber, providers, EventFilter } from 'ethers' +import { + BigNumber, + constants as ethersConstants, + Contract, + EventFilter, + providers, + Wallet, +} from 'ethers' import { orderBy } from 'lodash' import 'reflect-metadata' @@ -10,9 +17,15 @@ import { getContractFactory } from '@eth-optimism/contracts' import { getBobaContractAt } from '@boba/contracts' /* Imports: Interface */ -import { ChainInfo, DepositTeleportations, Disbursement } from './utils/types' -import { HistoryData } from './entity/HistoryData' -import { AppDataSource, historyDataRepository } from "./data-source"; +import { + AssetReceivedEvent, + ChainInfo, + DepositTeleportations, + Disbursement, + SupportedAssets, +} from './utils/types' +import { HistoryData } from './entities/HistoryData.entity' +import { historyDataRepository } from './data-source' interface TeleportationOptions { l2RpcProvider: providers.StaticJsonRpcProvider @@ -23,21 +36,20 @@ interface TeleportationOptions { // Address of the teleportation contract teleportationAddress: string - // Address of the L2 BOBA token - bobaTokenAddress: string - // Wallet disburserWallet: Wallet selectedBobaChains: ChainInfo[] + // Own chain to map token symbols to other networks + ownSupportedAssets: SupportedAssets + pollingInterval: number blockRangePerPolling: number } const optionSettings = {} -const bobaTokenAddressOnAltL2s = '0x4200000000000000000000000000000000000006' export class TeleportationService extends BaseService { constructor(options: TeleportationOptions) { @@ -46,8 +58,6 @@ export class TeleportationService extends BaseService { private state: { Teleportation: Contract - // This is only for Mainnet and Goerli L2s - BOBAToken: Contract // the chain is registered in the teleportation contract supportedChains: ChainInfo[] // the contract of the chain that users deposit token @@ -65,18 +75,11 @@ export class TeleportationService extends BaseService { this.options.teleportationAddress, this.options.disburserWallet ) + this.logger.info('Connected to Teleportation', { address: this.state.Teleportation.address, }) - this.logger.info('Connecting to BOBAToken contract...') - this.state.BOBAToken = getContractFactory('L2StandardERC20') - .attach(this.options.bobaTokenAddress) - .connect(this.options.disburserWallet) - this.logger.info('Connected to BOBAToken', { - address: this.state.BOBAToken.address, - }) - // check the disburser wallet is the disburser of the contract const disburserAddress = await this.state.Teleportation.disburser() if (disburserAddress !== this.options.disburserWallet.address) { @@ -92,12 +95,21 @@ export class TeleportationService extends BaseService { this.state.depositTeleportations = [] for (const chain of this.options.selectedBobaChains) { const chainId = chain.chainId - const isSupported = await this.state.Teleportation.supportedChains( + // assuming BOBA is enabled on supported networks to retain battle-tested logic + const bobaTokenContract = Object.keys(chain.supportedAssets).find( + (k) => chain.supportedAssets[k] === 'BOBA' + ) + const isSupported = await this.state.Teleportation.supportedTokens( + bobaTokenContract, chainId ) if (!isSupported) { throw new Error( - `Chain ${chainId} is not supported by the contract ${this.state.Teleportation.address}` + `Chain ${chainId} is not supported by the contract ${ + this.state.Teleportation.address + } on chain ${ + (await this.state.Teleportation.provider.getNetwork()).chainId + }` ) } else { this.state.supportedChains = [...this.state.supportedChains, chain] @@ -111,6 +123,7 @@ export class TeleportationService extends BaseService { const totalDeposits = await depositTeleportation.totalDeposits( this.options.chainId ) + this.state.depositTeleportations.push({ Teleportation: depositTeleportation, chainId, @@ -125,11 +138,11 @@ export class TeleportationService extends BaseService { protected async _start(): Promise { while (this.running) { for (const depositTeleportation of this.state.depositTeleportations) { - // search BobaReceived events + // search AssetReceived events const latestBlock = await depositTeleportation.Teleportation.provider.getBlockNumber() try { - const events = await this._watchTeleportation( + const events: AssetReceivedEvent[] = await this._watchTeleportation( depositTeleportation, latestBlock ) @@ -148,10 +161,16 @@ export class TeleportationService extends BaseService { } } + private getConnectedTokenContract(address: string): Contract { + return getContractFactory('L2StandardERC20') + .attach(address) + .connect(this.options.disburserWallet) + } + async _watchTeleportation( depositTeleportation: DepositTeleportations, latestBlock: number - ): Promise { + ): Promise { let lastBlock: number const chainId = depositTeleportation.chainId.toString() try { @@ -162,18 +181,17 @@ export class TeleportationService extends BaseService { // store the new deposit info await this._putDepositInfo(chainId, lastBlock) } - const events = await this._getEvents( + return await this._getEvents( depositTeleportation.Teleportation, - this.state.Teleportation.filters.BobaReceived(), + this.state.Teleportation.filters.AssetReceived(), lastBlock, latestBlock ) - return events } async _disburseTeleportation( depositTeleportation: DepositTeleportations, - events: any, + events: AssetReceivedEvent[], latestBlock: number ): Promise { const chainId = depositTeleportation.chainId @@ -185,35 +203,60 @@ export class TeleportationService extends BaseService { const lastDisbursement = await this.state.Teleportation.totalDisbursements(chainId) // eslint-disable-next-line prefer-const - let disbursement = [] - - for (const event of events) { - const sourceChainId = event.args.sourceChainId - const depositId = event.args.depositId - const amount = event.args.amount - const emitter = event.args.emitter - - // we disburse tokens only if depositId is greater or equal to the last disbursement - if (depositId.gte(lastDisbursement)) { - disbursement = [ - ...disbursement, - { - amount: amount.toString(), - addr: emitter, - depositId: depositId.toNumber(), - sourceChainId: sourceChainId.toString(), - }, - ] - this.logger.info( - `Found a new deposit - sourceChainId: ${sourceChainId.toString()} - depositId: ${depositId.toNumber()} - amount: ${amount.toString()} - emitter: ${emitter}` - ) + let disbursement: Disbursement[] = [] + + try { + for (const event of events) { + const sourceChainId: BigNumber = event.args.sourceChainId + const depositId = event.args.depositId + const amount = event.args.amount + const sourceChainTokenAddr = event.args.token + const emitter = event.args.emitter + const destChainId = event.args.toChainId + + // we disburse tokens only if depositId is greater or equal to the last disbursement + if (depositId.gte(lastDisbursement)) { + const destChainTokenAddr = + this._getSupportedDestChainTokenAddrBySourceChainTokenAddr( + sourceChainTokenAddr, + sourceChainId + ) + + const [isTokenSupported, , , , ,] = + await this.state.Teleportation.supportedTokens( + sourceChainTokenAddr, + sourceChainId + ) + if (!isTokenSupported) { + throw new Error( + `Token '${sourceChainTokenAddr}' not supported originating from chain '${sourceChainId}' with amount '${amount}'!` + ) + } else { + disbursement = [ + ...disbursement, + { + token: destChainTokenAddr, // token mapping for correct routing as addresses different on every network + amount: amount.toString(), + addr: emitter, + depositId: depositId.toNumber(), + sourceChainId: sourceChainId.toString(), + }, + ] + this.logger.info( + `Found a new deposit - sourceChainId: ${sourceChainId.toString()} - depositId: ${depositId.toNumber()} - amount: ${amount.toString()} - emitter: ${emitter} - token/native: ${sourceChainTokenAddr}` + ) + } + } } - } - // sort disbursement - disbursement = orderBy(disbursement, ['depositId'], ['asc']) - // disbure the token - await this._disburseTx(disbursement, chainId, latestBlock) + // sort disbursement + disbursement = orderBy(disbursement, ['depositId'], ['asc']) + // disbure the token but only if all disbursements could have been processed to avoid missing events due to updating the latestBlock + await this._disburseTx(disbursement, chainId, latestBlock) + } catch (e) { + // Catch outside loop to stop at first failing depositID as all subsequent disbursements as depositId = amountDisbursements and would fail when disbursing + this.logger.error(e.message) + } } } @@ -230,27 +273,36 @@ export class TeleportationService extends BaseService { let sliceEnd = numberOfDisbursement > 10 ? 10 : numberOfDisbursement while (sliceStart < numberOfDisbursement) { const slicedDisbursement = disbursement.slice(sliceStart, sliceEnd) - const totalDisbursements = slicedDisbursement.reduce((acc, cur) => { - return acc.add(BigNumber.from(cur.amount)) - }, BigNumber.from('0')) - if (this.options.bobaTokenAddress !== bobaTokenAddressOnAltL2s) { - // approve BOBA token - const approveTx = await this.state.BOBAToken.approve( - this.state.Teleportation.address, - totalDisbursements - ) - await approveTx.wait() - const disburseTx = await this.state.Teleportation.disburseBOBA( - slicedDisbursement - ) - await disburseTx.wait() - } else { - const disburseTx = await this.state.Teleportation.disburseNativeBOBA( - slicedDisbursement, - { value: totalDisbursements } + + // approve token(s), disbursements can be mixed - sum up token amounts per token + const tokens: Map = new Map() + const approvePending = [] + for (const disb of slicedDisbursement) { + tokens.set( + disb.token, + BigNumber.from(disb.amount).add(tokens.get(disb.token) ?? '0') ) - await disburseTx.wait() } + // do separate approves if necessary & sum up native requirement + let nativeValue: BigNumber = BigNumber.from('0') + for (const token of tokens.entries()) { + if (token[0] === ethersConstants.AddressZero) { + nativeValue = nativeValue.add(token[1]) + } else { + const approveTx = await this.getConnectedTokenContract( + token[0] + ).approve(this.state.Teleportation.address, token[1]) + approvePending.push(approveTx.wait()) + } + } + await Promise.all(approvePending) + + const disburseTx = await this.state.Teleportation.disburseAsset( + slicedDisbursement, + { value: nativeValue } + ) + await disburseTx.wait() + sliceStart = sliceEnd sliceEnd = Math.min(sliceEnd + 10, numberOfDisbursement) } @@ -290,6 +342,41 @@ export class TeleportationService extends BaseService { return events } + /** + * @dev Helper method for accessing the supportedAssets map via value (needed as we need it one way another as we don't save the ticker on-chain). + * @param sourceChainTokenAddr: Token/Asset address (ZeroAddr for native asset) on source network + * @param sourceChainId: ChainId the request is coming from + **/ + _getSupportedDestChainTokenAddrBySourceChainTokenAddr( + sourceChainTokenAddr: string, + sourceChainId: BigNumber | number + ) { + const srcChain: ChainInfo = this.state.supportedChains.find( + (c) => c.chainId.toString() === sourceChainId.toString() + ) + if (!srcChain) { + throw new Error( + `Source chain not configured/supported: ${srcChain} - ${sourceChainId} - supported: ${JSON.stringify( + this.state.supportedChains.map((c) => c.chainId) + )}` + ) + } + + const srcChainTokenSymbol = srcChain.supportedAssets[sourceChainTokenAddr] + + const supportedAsset = Object.entries(this.options.ownSupportedAssets).find( + ([address, tokenSymbol]) => { + return tokenSymbol === srcChainTokenSymbol + } + ) + if (!supportedAsset) { + throw new Error( + `Asset ${srcChainTokenSymbol} on chain destinationChain not configured but possibly supported on-chain` + ) + } + return supportedAsset[0] // return only address + } + async _putDepositInfo( chainId: number | string, latestBlock: number @@ -298,7 +385,16 @@ export class TeleportationService extends BaseService { const historyData = new HistoryData() historyData.chainId = chainId historyData.blockNo = latestBlock - await historyDataRepository.save(historyData) + if ( + await historyDataRepository.findOneBy({ chainId: historyData.chainId }) + ) { + await historyDataRepository.update( + { chainId: historyData.chainId }, + historyData + ) + } else { + await historyDataRepository.save(historyData) + } } catch (error) { this.logger.error(`Failed to put depositInfo! - ${error}`) } diff --git a/packages/boba/teleportation/src/utils/chains.ts b/packages/boba/teleportation/src/utils/chains.ts index 85a40492da..85ef8d0c56 100644 --- a/packages/boba/teleportation/src/utils/chains.ts +++ b/packages/boba/teleportation/src/utils/chains.ts @@ -1,19 +1,36 @@ -export const BobaChains = { +export interface IBobaChains { + [chainId: number]: { + url: string + testnet: boolean + name: string + teleportationAddress: string + height: number + supportedAssets: { + [address: string]: string // symbol (MUST BE UNIQUE) + } + } +} + +/** + * @dev Chain configs + * @property supportedAssets: BOBA as fee token only supported for EOAs, since Teleporter consists of a contract & the disburser wallet (assuming ETH fee) everything with 0x0 should be fine. + **/ +export const BobaChains: IBobaChains = { + // TODO: Consider using AddressManager or AddressPackage instead + + //#region boba_networks 288: { url: 'https://replica.boba.network', testnet: false, name: 'Boba Ethereum Mainnet', teleportationAddress: '0xd68809330075C792C171C450B983F4D18128e9BF', height: 873302, - BobaTokenAddress: '0xa18bF3994C0Cc6E3b63ac420308E5383f53120D7', - }, - 1294: { - url: 'https://replica.bobabeam.boba.network', - testnet: false, - name: 'Bobabeam', - teleportationAddress: '0xd68809330075C792C171C450B983F4D18128e9BF', - height: 479856, - BobaTokenAddress: '0x4200000000000000000000000000000000000006', + supportedAssets: { + '0x0000000000000000000000000000000000000000': 'ETH', + '0xa18bF3994C0Cc6E3b63ac420308E5383f53120D7': 'BOBA', + '0x5DE1677344D3Cb0D7D465c10b72A8f60699C062d': 'USDT', + '0x68ac1623ACf9eB9F88b65B5F229fE3e2c0d5789e': 'BNB', + }, }, 43288: { url: 'https://replica.avax.boba.network', @@ -21,7 +38,10 @@ export const BobaChains = { name: 'Boba Avalanche Mainnet', teleportationAddress: '0xd68809330075C792C171C450B983F4D18128e9BF', height: 25078, - BobaTokenAddress: '0x4200000000000000000000000000000000000006', + supportedAssets: { + '0x0000000000000000000000000000000000000000': 'AVAX', + '0x4200000000000000000000000000000000000006': 'BOBA', + }, }, 56288: { url: 'https://replica.bnb.boba.network', @@ -29,54 +49,71 @@ export const BobaChains = { name: 'Boba BNB Mainnet', teleportationAddress: '0xd68809330075C792C171C450B983F4D18128e9BF', height: 3393, - BobaTokenAddress: '0x4200000000000000000000000000000000000006', - }, - 301: { - url: 'https://replica.bobaopera.boba.network', - testnet: false, - name: 'Bobaopera', - teleportationAddress: '0xd68809330075C792C171C450B983F4D18128e9BF', - height: 10604, - BobaTokenAddress: '0x4200000000000000000000000000000000000006', + supportedAssets: { + '0x0000000000000000000000000000000000000000': 'BNB', + '0x4200000000000000000000000000000000000006': 'BOBA', + }, }, 2888: { url: 'https://replica.goerli.boba.network', testnet: true, name: 'Boba Ethereum Goerli', - teleportationAddress: '0xd68809330075C792C171C450B983F4D18128e9BF', - height: 59, - BobaTokenAddress: '0x4200000000000000000000000000000000000023', - }, - 1297: { - url: 'https://replica.bobabase.boba.network', - testnet: true, - name: 'Bobabase', - teleportationAddress: '0xd68809330075C792C171C450B983F4D18128e9BF', - height: 304189, - BobaTokenAddress: '0x4200000000000000000000000000000000000006', + teleportationAddress: '0x2af1C32E1dE8e041B7E45525A1Ca3C519Fac312F', + height: 3820, + supportedAssets: { + '0x0000000000000000000000000000000000000000': 'ETH', + '0x4200000000000000000000000000000000000023': 'BOBA', + }, }, 4328: { url: 'https://replica.testnet.avax.boba.network', testnet: true, name: 'Boba Avalanche Testnet', - teleportationAddress: '0xd68809330075C792C171C450B983F4D18128e9BF', - height: 489, - BobaTokenAddress: '0x4200000000000000000000000000000000000006', + teleportationAddress: '0x9A57d90E80BE60340f804fd2D0373dd34AB934A2', + height: 3148, + supportedAssets: { + '0x4200000000000000000000000000000000000023': 'AVAX', + '0x0000000000000000000000000000000000000000': 'BOBA', + }, }, 9728: { url: 'https://replica.testnet.bnb.boba.network', testnet: true, name: 'Boba BNB Testnet', - teleportationAddress: '0xd68809330075C792C171C450B983F4D18128e9BF', - height: 3288, - BobaTokenAddress: '0x4200000000000000000000000000000000000006', + teleportationAddress: '0xE3B5FB4CDa3C4c58A804e8856B5eC81D87972512', + height: 240152, + supportedAssets: { + '0x4200000000000000000000000000000000000023': 'BNB', + '0x0000000000000000000000000000000000000000': 'BOBA', + }, + }, + //#endregion + //#region l1 + 1: { + url: 'https://eth.llamarpc.com', + testnet: false, + name: 'Ethereum Mainnet', + teleportationAddress: '0x0', + height: 17565090, + supportedAssets: { + '0x0000000000000000000000000000000000000000': 'ETH', + '0x42bBFa2e77757C645eeaAd1655E0911a7553Efbc': 'BOBA', + '0xdAC17F958D2ee523a2206206994597C13D831ec7': 'USDT', + '0xB8c77482e45F1F44dE1745F52C74426C631bDD52': 'BNB', + }, }, - 4051: { - url: 'https://replica.testnet.bobaopera.boba.network', + 5: { + url: 'https://goerli.gateway.tenderly.co', testnet: true, - name: 'Bobaopera Testnet', - teleportationAddress: '0xd68809330075C792C171C450B983F4D18128e9BF', - height: 299, - BobaTokenAddress: '0x4200000000000000000000000000000000000006', + name: 'Goerli Testnet', + teleportationAddress: '0x9A597f96899d9cc7Ba0Bd8a4148d7B7Ed6AA0300', + height: 9244943, + supportedAssets: { + '0x0000000000000000000000000000000000000000': 'ETH', + '0xC2C527C0CACF457746Bd31B2a698Fe89de2b6d49': 'USDT', + '0xFC1C82c5EdeB51082CF30FDDb434D2cBDA1f6924': 'BNB', + '0xeCCD355862591CBB4bB7E7dD55072070ee3d0fC1': 'BOBA', + }, }, + //#endregion } diff --git a/packages/boba/teleportation/src/utils/types.ts b/packages/boba/teleportation/src/utils/types.ts index 3455de30d4..0b276ce0d3 100644 --- a/packages/boba/teleportation/src/utils/types.ts +++ b/packages/boba/teleportation/src/utils/types.ts @@ -1,5 +1,20 @@ import { BigNumber, Contract, providers } from 'ethers' +export interface SupportedAssets { + [address: string]: string // symbol (MUST BE UNIQUE) +} + +export interface AssetReceivedEvent { + args: { + token: string + sourceChainId: BigNumber + toChainId: BigNumber + depositId: BigNumber + emitter: string + amount: BigNumber + } +} + export interface ChainInfo { chainId: number url: string @@ -8,7 +23,7 @@ export interface ChainInfo { name: string teleportationAddress: string height: number - BobaTokenAddress?: string + supportedAssets: SupportedAssets } export interface DepositTeleportations { @@ -20,6 +35,8 @@ export interface DepositTeleportations { } export interface Disbursement { + /** @dev Ignored for native disbursements */ + token: string amount: string addr: string sourceChainId: number | string