From 6b001292372c6c27facd0a6f67f7f8fcfd90c5ab Mon Sep 17 00:00:00 2001 From: cby3149 Date: Wed, 6 Apr 2022 15:25:23 -0700 Subject: [PATCH 1/6] Add billing contract in LP and standard bridge --- integration-tests/test/mrf_lp.spec.ts | 174 +++++++++++---- ...t_burn.spec.ts => routed_exit_fee.spec.ts} | 201 +++++++++++++----- .../contracts/DiscretionaryExitFee.sol | 68 ++++++ .../contracts/LP/L2LiquidityPool.sol | 38 +++- .../contracts/deploy/015-ExitBurn.deploy.ts | 50 ----- .../contracts/deploy/015-ExitFee.deploy.ts | 52 +++++ .../deploy/019-L2BillingContract.deploy.ts | 133 ++++++++++++ ...{019-Dump.deploy.ts => 020-Dump.deploy.ts} | 0 8 files changed, 566 insertions(+), 150 deletions(-) rename integration-tests/test/{routed_exit_burn.spec.ts => routed_exit_fee.spec.ts} (53%) create mode 100644 packages/boba/contracts/contracts/DiscretionaryExitFee.sol delete mode 100644 packages/boba/contracts/deploy/015-ExitBurn.deploy.ts create mode 100644 packages/boba/contracts/deploy/015-ExitFee.deploy.ts create mode 100644 packages/boba/contracts/deploy/019-L2BillingContract.deploy.ts rename packages/boba/contracts/deploy/{019-Dump.deploy.ts => 020-Dump.deploy.ts} (100%) diff --git a/integration-tests/test/mrf_lp.spec.ts b/integration-tests/test/mrf_lp.spec.ts index 897bcf94bf..c9b2762f6d 100644 --- a/integration-tests/test/mrf_lp.spec.ts +++ b/integration-tests/test/mrf_lp.spec.ts @@ -13,6 +13,7 @@ import L2TokenPoolJson from '@boba/contracts/artifacts/contracts/TokenPool.sol/T import OMGLikeTokenJson from '@boba/contracts/artifacts/contracts/test-helpers/OMGLikeToken.sol/OMGLikeToken.json' import L2GovernanceERC20Json from '@boba/contracts/artifacts/contracts/standards/L2GovernanceERC20.sol/L2GovernanceERC20.json' import xL2GovernanceERC20Json from '@boba/contracts/artifacts/contracts/standards/xL2GovernanceERC20.sol/xL2GovernanceERC20.json' +import L2BillingContractJson from '@boba/contracts/artifacts/contracts/L2BillingContract.sol/L2BillingContract.json' import { OptimismEnv } from './shared/env' @@ -40,6 +41,8 @@ describe('Liquidity Pool Test', async () => { let L2BOBAToken: Contract let xBOBAToken: Contract + let BOBABillingContract: Contract + let env: OptimismEnv const initialSupply = utils.parseEther('10000000000') @@ -138,6 +141,12 @@ describe('Liquidity Pool Test', async () => { xL2GovernanceERC20Json.abi, env.l2Wallet ) + + BOBABillingContract = new Contract( + env.addressesBOBA.Proxy__BobaBillingContract, + L2BillingContractJson.abi, + env.l2Wallet + ) }) it('{tag:mrf} should deposit 10000 TEST ERC20 token from L1 to L2', async () => { @@ -406,8 +415,20 @@ describe('Liquidity Pool Test', async () => { fastExitAmount, { gasLimit: 7000000 } ) - await approveKateL2TX.wait() + + // Approve BOBA + const exitFee = await BOBABillingContract.exitFee() + const approveBOBATX = await L2BOBAToken.connect(env.l2Wallet_3).approve( + L2LiquidityPool.address, + exitFee + ) + await approveBOBATX.wait() + + const BobaBalanceBefore = await L2BOBAToken.balanceOf( + BOBABillingContract.address + ) + const depositTx = await env.waitForXDomainTransactionFast( L2LiquidityPool.connect(env.l2Wallet_3).clientDepositL2( fastExitAmount, @@ -416,6 +437,11 @@ describe('Liquidity Pool Test', async () => { ) ) + const BobaBalanceAfter = await L2BOBAToken.balanceOf( + BOBABillingContract.address + ) + expect(BobaBalanceAfter).to.deep.eq(BobaBalanceBefore.add(exitFee)) + const poolInfo = await L1LiquidityPool.poolInfo(L1ERC20.address) const ownerRewardFeeRate = await L1LiquidityPool.ownerRewardFeeRate() @@ -697,9 +723,9 @@ describe('Liquidity Pool Test', async () => { ) }) - /* In this test, we provide liquidity X to a pool, - but then trigger a X + 1000 liquidity request. - If the system is working correctly, this should trigger a revert + /* In this test, we provide liquidity X to a pool, + but then trigger a X + 1000 liquidity request. + If the system is working correctly, this should trigger a revert */ // it('{tag:mrf} 1 should revert unfulfillable swap-offs', async () => { @@ -794,6 +820,14 @@ describe('Liquidity Pool Test', async () => { // ) // ) + // Approve BOBA + const exitFee = await BOBABillingContract.exitFee() + const approveBOBATX = await L2BOBAToken.connect(env.l2Wallet).approve( + L2LiquidityPool.address, + exitFee + ) + await approveBOBATX.wait() + const ret = await env.waitForXDomainTransactionFast( L2LiquidityPool.connect(env.l2Wallet).clientDepositL2( fastExitAmount, @@ -1353,6 +1387,14 @@ describe('Liquidity Pool Test', async () => { it('{tag:mrf} should fail to fast exit L2 with incorrect inputs', async () => { const fastExitAmount = utils.parseEther('10') + // Approve BOBA + const exitFee = await BOBABillingContract.exitFee() + const approveBOBATX = await L2BOBAToken.connect(env.l2Wallet).approve( + L2LiquidityPool.address, + exitFee + ) + await approveBOBATX.wait() + await expect( L2LiquidityPool.connect(env.l2Wallet).clientDepositL2( fastExitAmount, @@ -1378,6 +1420,14 @@ describe('Liquidity Pool Test', async () => { ethers.constants.AddressZero ) + // Approve BOBA + const exitFee = await BOBABillingContract.exitFee() + const approveBOBATX = await L2BOBAToken.connect(env.l2Wallet).approve( + L2LiquidityPool.address, + exitFee + ) + await approveBOBATX.wait() + const depositTx = await env.waitForXDomainTransactionFast( L2LiquidityPool.connect(env.l2Wallet).clientDepositL2( fastExitAmount, @@ -1671,6 +1721,14 @@ describe('Liquidity Pool Test', async () => { // ) // ) + // Approve BOBA + const exitFee = await BOBABillingContract.exitFee() + const approveBOBATX = await L2BOBAToken.connect(env.l2Wallet).approve( + L2LiquidityPool.address, + exitFee + ) + await approveBOBATX.wait() + await env.waitForXDomainTransactionFast( L2LiquidityPool.connect(env.l2Wallet).clientDepositL2( fastExitAmount, @@ -1727,39 +1785,27 @@ describe('Liquidity Pool Test', async () => { }) }) - describe('Relay gas burn tests', async () => { - it('{tag:mrf} should not allow updating extraGasRelay for non-owner', async () => { - const newExtraGasRelay = 500000 + describe('Exit fee tests', async () => { + it('{tag:mrf} should not allow updating exit fee for non-owner', async () => { + const nexExitFee = ethers.utils.parseEther('120') await expect( - L2LiquidityPool.connect(env.l2Wallet_2).configureExtraGasRelay( - newExtraGasRelay - ) - ).to.be.revertedWith('Caller is not the gasPriceOracle owner') + BOBABillingContract.connect(env.l2Wallet_2).updateExitFee(nexExitFee) + ).to.be.revertedWith('Caller is not the owner') }) - it('{tag:mrf} should allow updating extraGasRelay for owner', async () => { - // approximate and set new extra gas to over it for tests - const approveBobL2TX = await L2ERC20.approve( - L2LiquidityPool.address, - utils.parseEther('10') - ) - await approveBobL2TX.wait() - const estimatedGas = await L2LiquidityPool.estimateGas.clientDepositL2( - utils.parseEther('10'), - L2ERC20.address - ) - - const newExtraGasRelay = estimatedGas.mul(2) - const configureTx = await L2LiquidityPool.connect( - env.l2Wallet_4 - ).configureExtraGasRelay(newExtraGasRelay) + it('{tag:mrf} should allow updating exit fee for owner', async () => { + const exitFeeBefore = await BOBABillingContract.exitFee() + const newExitFee = exitFeeBefore.mul(2) + const configureTx = await BOBABillingContract.connect( + env.l2Wallet + ).updateExitFee(newExitFee) await configureTx.wait() - const updatedExtraGasRelay = await L2LiquidityPool.extraGasRelay() - expect(updatedExtraGasRelay).to.eq(newExtraGasRelay) + const updatedExitFee = await BOBABillingContract.exitFee() + expect(newExitFee).to.eq(updatedExitFee) }) - it('{tag:mrf} should be able to fast exit with correct added gas', async () => { + it('{tag:mrf} should be able to fast exit with correct exit fee', async () => { const fastExitAmount = utils.parseEther('10') const preBobL1ERC20Balance = await L1ERC20.balanceOf(env.l1Wallet.address) @@ -1773,6 +1819,21 @@ describe('Liquidity Pool Test', async () => { ) await approveBobL2TX.wait() + // Approve BOBA + const exitFee = await BOBABillingContract.exitFee() + const approveBOBATX = await L2BOBAToken.connect(env.l2Wallet).approve( + L2LiquidityPool.address, + exitFee + ) + await approveBOBATX.wait() + + const BobBobaBalanceBefore = await L2BOBAToken.balanceOf( + env.l2Wallet.address + ) + const billingContractBalanceBefore = await L2BOBAToken.balanceOf( + BOBABillingContract.address + ) + const depositTx = await env.waitForXDomainTransactionFast( L2LiquidityPool.clientDepositL2(fastExitAmount, L2ERC20.address) ) @@ -1784,21 +1845,56 @@ describe('Liquidity Pool Test', async () => { const postBobL1ERC20Balance = await L1ERC20.balanceOf( env.l1Wallet.address ) + const BobBobaBalanceAfter = await L2BOBAToken.balanceOf( + env.l2Wallet.address + ) + const billingContractBalanceAfter = await L2BOBAToken.balanceOf( + BOBABillingContract.address + ) expect(postBobL1ERC20Balance).to.deep.eq( preBobL1ERC20Balance.add(fastExitAmount.mul(remainingPercent).div(1000)) ) - const extraGasRelay = await L2LiquidityPool.extraGasRelay() - expect(depositTx.receipt.gasUsed).to.be.gt(extraGasRelay) + expect(billingContractBalanceAfter).to.deep.eq( + billingContractBalanceBefore.add(exitFee) + ) + expect(BobBobaBalanceAfter).to.deep.eq(BobBobaBalanceBefore.sub(exitFee)) + }) - // update it back to zero for tests - const configureTx = await L2LiquidityPool.connect( - env.l2Wallet_4 - ).configureExtraGasRelay(0) - await configureTx.wait() - const finalExtraGasRelay = await L2LiquidityPool.extraGasRelay() - expect(finalExtraGasRelay).to.eq(0) + it('{tag:mrf} should not fast exit without Boba', async () => { + const fastExitAmount = utils.parseEther('10') + + const newWallet = ethers.Wallet.createRandom().connect(env.l2Provider) + await env.l2Wallet.sendTransaction({ + to: newWallet.address, + value: ethers.utils.parseEther('1'), + }) + + await expect( + L2LiquidityPool.connect(newWallet).clientDepositL2( + fastExitAmount, + L2ERC20.address + ) + ).to.be.revertedWith( + 'execution reverted: ERC20: transfer amount exceeds balance' + ) + }) + + it('{tag:mrf} should not fast exit without approving Boba', async () => { + const fastExitAmount = utils.parseEther('10') + + const approveBobL2TX = await L2ERC20.approve( + L2LiquidityPool.address, + utils.parseEther('10') + ) + await approveBobL2TX.wait() + + await expect( + L2LiquidityPool.clientDepositL2(fastExitAmount, L2ERC20.address) + ).to.be.revertedWith( + 'execution reverted: ERC20: transfer amount exceeds allowance' + ) }) }) diff --git a/integration-tests/test/routed_exit_burn.spec.ts b/integration-tests/test/routed_exit_fee.spec.ts similarity index 53% rename from integration-tests/test/routed_exit_burn.spec.ts rename to integration-tests/test/routed_exit_fee.spec.ts index 1651ace97b..e76965be84 100644 --- a/integration-tests/test/routed_exit_burn.spec.ts +++ b/integration-tests/test/routed_exit_fee.spec.ts @@ -4,25 +4,30 @@ chai.use(chaiAsPromised) import { Contract, ContractFactory, BigNumber, utils, ethers } from 'ethers' import { getContractFactory } from '@eth-optimism/contracts' -import DiscretionaryExitBurnJson from '@boba/contracts/artifacts/contracts/DiscretionaryExitBurn.sol/DiscretionaryExitBurn.json' +import DiscretionaryExitFeeJson from '@boba/contracts/artifacts/contracts/DiscretionaryExitFee.sol/DiscretionaryExitFee.json' import L1ERC20Json from '@boba/contracts/artifacts/contracts/test-helpers/L1ERC20.sol/L1ERC20.json' import OMGLikeTokenJson from '@boba/contracts/artifacts/contracts/test-helpers/OMGLikeToken.sol/OMGLikeToken.json' +import L2BillingContractJson from '@boba/contracts/artifacts/contracts/L2BillingContract.sol/L2BillingContract.json' +import L2GovernanceERC20Json from '@boba/contracts/artifacts/contracts/standards/L2GovernanceERC20.sol/L2GovernanceERC20.json' import { OptimismEnv } from './shared/env' -describe('Standard Exit burn', async () => { +describe('Standard Exit Fee', async () => { let Factory__L1ERC20: ContractFactory let Factory__L2ERC20: ContractFactory - let Factory__ExitBurn: ContractFactory + let Factory__ExitFeeContract: ContractFactory let L1ERC20: Contract let L2ERC20: Contract let L1StandardBridge: Contract - let ExitBurn: Contract + let ExitFeeContract: Contract let OMGLIkeToken: Contract let L2OMGLikeToken: Contract + let L2BOBAToken: Contract + let BOBABillingContract: Contract + let env: OptimismEnv const initialSupply = utils.parseEther('10000000000') @@ -54,13 +59,20 @@ describe('Standard Exit burn', async () => { const L2StandardBridgeAddress = await L1StandardBridge.l2TokenBridge() - Factory__ExitBurn = new ContractFactory( - DiscretionaryExitBurnJson.abi, - DiscretionaryExitBurnJson.bytecode, + Factory__ExitFeeContract = new ContractFactory( + DiscretionaryExitFeeJson.abi, + DiscretionaryExitFeeJson.bytecode, env.l2Wallet ) - ExitBurn = await Factory__ExitBurn.deploy(L2StandardBridgeAddress) + ExitFeeContract = await Factory__ExitFeeContract.deploy( + L2StandardBridgeAddress, + env.addressesBOBA.TOKENS.BOBA.L2 + ) + + await ExitFeeContract.configureBillingContractAddress( + env.addressesBOBA.Proxy__BobaBillingContract + ) //we deploy a new erc20, so tests won't fail on a rerun on the same contracts L1ERC20 = await Factory__L1ERC20.deploy( @@ -93,6 +105,18 @@ describe('Standard Exit burn', async () => { 18 ) await L2OMGLikeToken.deployTransaction.wait() + + L2BOBAToken = new Contract( + env.addressesBOBA.TOKENS.BOBA.L2, + L2GovernanceERC20Json.abi, + env.l2Wallet + ) + + BOBABillingContract = new Contract( + env.addressesBOBA.Proxy__BobaBillingContract, + L2BillingContractJson.abi, + env.l2Wallet + ) }) describe('Relay config priviledges', async () => { @@ -116,38 +140,23 @@ describe('Standard Exit burn', async () => { ) }) - it('{tag:other} should not allow updating extraGasRelay for non-owner', async () => { - const newExtraGasRelay = 500000 + it('{tag:other} should not allow updating exit fee for non-owner', async () => { + const nexExitFee = ethers.utils.parseEther('120') await expect( - ExitBurn.connect(env.l2Wallet_2).configureExtraGasRelay( - newExtraGasRelay - ) - ).to.be.revertedWith('caller is not the gasPriceOracle owner') + BOBABillingContract.connect(env.l2Wallet_2).updateExitFee(nexExitFee) + ).to.be.revertedWith('Caller is not the owner') }) - it('{tag:other} should allow updating extraGasRelay for owner', async () => { - // approximate and set new extra gas to over it for tests - const approveBobL2TX = await L2ERC20.approve( - ExitBurn.address, - utils.parseEther('10') - ) - await approveBobL2TX.wait() - - const estimatedGas = await ExitBurn.estimateGas.burnAndWithdraw( - L2ERC20.address, - utils.parseEther('10'), - 9999999, - ethers.utils.formatBytes32String(new Date().getTime().toString()) - ) - - const newExtraGasRelay = estimatedGas.mul(2) - const configureTx = await ExitBurn.connect( - env.l2Wallet_4 - ).configureExtraGasRelay(newExtraGasRelay) + it('{tag:other} should allow updating exit fee for owner', async () => { + const exitFeeBefore = await BOBABillingContract.exitFee() + const newExitFee = exitFeeBefore.mul(2) + const configureTx = await BOBABillingContract.connect( + env.l2Wallet + ).updateExitFee(newExitFee) await configureTx.wait() - const updatedExtraGasRelay = await ExitBurn.extraGasRelay() - expect(updatedExtraGasRelay).to.eq(newExtraGasRelay) + const updatedExitFee = await BOBABillingContract.exitFee() + expect(newExitFee).to.eq(updatedExitFee) }) }) @@ -172,7 +181,7 @@ describe('Standard Exit burn', async () => { ) }) - it('{tag:other} should burn and withdraw erc20', async () => { + it('{tag:other} should pay exit fee and withdraw erc20', async () => { const preBalanceExitorL1 = await L1ERC20.balanceOf(env.l1Wallet.address) const preBalanceExitorL2 = await L2ERC20.balanceOf(env.l2Wallet.address) @@ -180,13 +189,26 @@ describe('Standard Exit burn', async () => { const exitAmount = preBalanceExitorL2 // approve const approveL2ERC20TX = await L2ERC20.approve( - ExitBurn.address, + ExitFeeContract.address, exitAmount ) await approveL2ERC20TX.wait() + // Approve BOBA + const exitFee = await BOBABillingContract.exitFee() + const approveBOBATX = await L2BOBAToken.connect(env.l2Wallet).approve( + ExitFeeContract.address, + exitFee + ) + await approveBOBATX.wait() + + const preBobaBalanceBillingContract = await L2BOBAToken.balanceOf( + BOBABillingContract.address + ) + const preBobaBalance = await L2BOBAToken.balanceOf(env.l2Wallet.address) + await env.waitForXDomainTransaction( - ExitBurn.burnAndWithdraw( + ExitFeeContract.payAndWithdraw( L2ERC20.address, exitAmount, 9999999, @@ -196,11 +218,21 @@ describe('Standard Exit burn', async () => { const postBalanceExitorL1 = await L1ERC20.balanceOf(env.l1Wallet.address) const postBalanceExitorL2 = await L2ERC20.balanceOf(env.l2Wallet.address) - const ExitBurnContractBalance = await L2ERC20.balanceOf(ExitBurn.address) + const ExitFeeContractBalance = await L2ERC20.balanceOf( + ExitFeeContract.address + ) + const postBobaBalanceBillingContract = await L2BOBAToken.balanceOf( + BOBABillingContract.address + ) + const postBobaBalance = await L2BOBAToken.balanceOf(env.l2Wallet.address) expect(postBalanceExitorL2).to.eq(preBalanceExitorL2.sub(exitAmount)) expect(postBalanceExitorL1).to.eq(preBalanceExitorL1.add(exitAmount)) - expect(ExitBurnContractBalance).to.eq(0) + expect(ExitFeeContractBalance).to.eq(0) + expect(postBobaBalanceBillingContract).to.eq( + preBobaBalanceBillingContract.add(exitFee) + ) + expect(postBobaBalance).to.eq(preBobaBalance.sub(exitFee)) }) it('{tag:other} should fail if not enough erc20 balance', async () => { @@ -209,8 +241,16 @@ describe('Standard Exit burn', async () => { expect(preBalanceExitorL2).to.eq(0) const exitAmount = utils.parseEther('10') + // Approve BOBA + const exitFee = await BOBABillingContract.exitFee() + const approveBOBATX = await L2BOBAToken.connect(env.l2Wallet).approve( + ExitFeeContract.address, + exitFee + ) + await approveBOBATX.wait() + await expect( - ExitBurn.burnAndWithdraw( + ExitFeeContract.payAndWithdraw( L2ERC20.address, exitAmount, 9999999, @@ -218,6 +258,49 @@ describe('Standard Exit burn', async () => { ) ).to.be.revertedWith('ERC20: transfer amount exceeds balance') }) + + it('{tag:other} should fail if not enough Boba balance', async () => { + const exitAmount = utils.parseEther('10') + + const newWallet = ethers.Wallet.createRandom().connect(env.l2Provider) + await env.l2Wallet.sendTransaction({ + to: newWallet.address, + value: ethers.utils.parseEther('1'), + }) + + await expect( + ExitFeeContract.payAndWithdraw( + L2ERC20.address, + exitAmount, + 9999999, + ethers.utils.formatBytes32String(new Date().getTime().toString()) + ) + ).to.be.revertedWith( + 'execution reverted: ERC20: transfer amount exceeds balance' + ) + }) + + it('{tag:other} should fail if not approving Boba', async () => { + const exitAmount = utils.parseEther('10') + + // Approve BOBA + const approveBOBATX = await L2BOBAToken.connect(env.l2Wallet).approve( + ExitFeeContract.address, + 0 + ) + await approveBOBATX.wait() + + await expect( + ExitFeeContract.payAndWithdraw( + L2ERC20.address, + exitAmount, + 9999999, + ethers.utils.formatBytes32String(new Date().getTime().toString()) + ) + ).to.be.revertedWith( + 'execution reverted: ERC20: transfer amount exceeds allowance' + ) + }) }) describe('Eth withdraw', async () => { @@ -240,8 +323,21 @@ describe('Standard Exit burn', async () => { expect(preBalanceExitorL2).to.not.eq(0) const exitAmount = utils.parseEther('10') + // Approve BOBA + const exitFee = await BOBABillingContract.exitFee() + const approveBOBATX = await L2BOBAToken.connect(env.l2Wallet).approve( + ExitFeeContract.address, + exitFee + ) + await approveBOBATX.wait() + + const preBobaBalanceBillingContract = await L2BOBAToken.balanceOf( + BOBABillingContract.address + ) + const preBobaBalance = await L2BOBAToken.balanceOf(env.l2Wallet.address) + await env.waitForXDomainTransaction( - ExitBurn.burnAndWithdraw( + ExitFeeContract.payAndWithdraw( env.ovmEth.address, exitAmount, 9999999, @@ -252,20 +348,21 @@ describe('Standard Exit burn', async () => { const postBalanceExitorL1 = await env.l1Wallet.getBalance() const postBalanceExitorL2 = await env.l2Wallet.getBalance() - const ExitBurnContractBalance = await env.l2Provider.getBalance( - ExitBurn.address + const ExitFeeContractBalance = await env.l2Provider.getBalance( + ExitFeeContract.address ) + const postBobaBalanceBillingContract = await L2BOBAToken.balanceOf( + BOBABillingContract.address + ) + const postBobaBalance = await L2BOBAToken.balanceOf(env.l2Wallet.address) expect(postBalanceExitorL2).to.be.lt(preBalanceExitorL2.sub(exitAmount)) - const expectedGas = preBalanceExitorL2 - .sub(exitAmount) - .sub(postBalanceExitorL2) - - // gas oracle updates the overhead and l1BaseFee, - // so it's not correct to expect the expectedGast is small than 1000000000 - // expect(expectedGas).to.lt(BigNumber.from(1000000000)) expect(postBalanceExitorL1).to.eq(preBalanceExitorL1.add(exitAmount)) - expect(ExitBurnContractBalance).to.eq(0) + expect(ExitFeeContractBalance).to.eq(0) + expect(postBobaBalanceBillingContract).to.eq( + preBobaBalanceBillingContract.add(exitFee) + ) + expect(postBobaBalance).to.eq(preBobaBalance.sub(exitFee)) }) }) }) diff --git a/packages/boba/contracts/contracts/DiscretionaryExitFee.sol b/packages/boba/contracts/contracts/DiscretionaryExitFee.sol new file mode 100644 index 0000000000..edc8c87cab --- /dev/null +++ b/packages/boba/contracts/contracts/DiscretionaryExitFee.sol @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: MIT +pragma solidity >0.7.5; + +import "@eth-optimism/contracts/contracts/libraries/constants/Lib_PredeployAddresses.sol"; + +import "@eth-optimism/contracts/contracts/L2/messaging/IL2ERC20Bridge.sol"; +import "@eth-optimism/contracts/contracts/L2/predeploys/OVM_GasPriceOracle.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "@openzeppelin/contracts/utils/math/SafeMath.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; + +import "./L2BillingContract.sol"; + +contract DiscretionaryExitFee is Ownable { + using SafeERC20 for IERC20; + using SafeMath for uint256; + + address public l2Bridge; + address public BOBAAddress; + address public billingContractAddress; + + constructor(address _l2Bridge, address _BOBAAddress) { + l2Bridge = _l2Bridge; + BOBAAddress = _BOBAAddress; + } + + modifier onlyWithBillingContract() { + require(billingContractAddress != address(0), "Billing contract address is not set"); + _; + } + + function configureBillingContractAddress( + address _billingContractAddress + ) + public + onlyOwner() + { + require(_billingContractAddress != address(0), "Billing contract address cannot be zero"); + billingContractAddress = _billingContractAddress; + } + + function payAndWithdraw( + address _l2Token, + uint256 _amount, + uint32 _l1Gas, + bytes calldata _data + ) external payable onlyWithBillingContract { + // Collect the exit fee + L2BillingContract billingContract = L2BillingContract(billingContractAddress); + IERC20(BOBAAddress).safeTransferFrom(msg.sender, billingContractAddress, billingContract.exitFee()); + + require(msg.value != 0 || _l2Token != Lib_PredeployAddresses.OVM_ETH, "Amount Incorrect"); + + if (msg.value != 0) { + // override the _amount and token address + _amount = msg.value; + _l2Token = Lib_PredeployAddresses.OVM_ETH; + } + + // transfer funds if users deposit ERC20 + if (_l2Token != Lib_PredeployAddresses.OVM_ETH) { + IERC20(_l2Token).safeTransferFrom(msg.sender, address(this), _amount); + } + + // call withdrawTo on the l2Bridge + IL2ERC20Bridge(l2Bridge).withdrawTo(_l2Token, msg.sender, _amount, _l1Gas, _data); + } +} diff --git a/packages/boba/contracts/contracts/LP/L2LiquidityPool.sol b/packages/boba/contracts/contracts/LP/L2LiquidityPool.sol index 712879fcc4..435ac9f975 100644 --- a/packages/boba/contracts/contracts/LP/L2LiquidityPool.sol +++ b/packages/boba/contracts/contracts/LP/L2LiquidityPool.sol @@ -17,6 +17,7 @@ import "@eth-optimism/contracts/contracts/L2/messaging/L2StandardBridge.sol"; /* External Imports */ import "../standards/xL2GovernanceERC20.sol"; +import "../L2BillingContract.sol"; /** * @dev An L2 LiquidityPool implementation @@ -109,9 +110,12 @@ contract L2LiquidityPool is CrossDomainEnabled, ReentrancyGuardUpgradeable, Paus address public xBOBAAddress; address public BOBAAddress; - // mapping use address to the status of xBOBA + // mapping user address to the status of xBOBA mapping(address => bool) public xBOBAStatus; + // billing contract address + address public billingContractAddress; + /******************** * Event * ********************/ @@ -217,6 +221,11 @@ contract L2LiquidityPool is CrossDomainEnabled, ReentrancyGuardUpgradeable, Paus _; } + modifier onlyWithBillingContract() { + require(billingContractAddress != address(0), "Billing contract address is not set"); + _; + } + /******************** * Public Functions * ********************/ @@ -360,6 +369,21 @@ contract L2LiquidityPool is CrossDomainEnabled, ReentrancyGuardUpgradeable, Paus DEFAULT_FINALIZE_WITHDRAWAL_L1_GAS = _l1GasFee; } + /** + * @dev Configure billing contract address. + * + * @param _billingContractAddress billing contract address + */ + function configureBillingContractAddress( + address _billingContractAddress + ) + public + onlyOwner() + { + require(_billingContractAddress != address(0), "Billing contract address cannot be zero"); + billingContractAddress = _billingContractAddress; + } + /** * @dev Return user reward fee rate. * @@ -615,15 +639,11 @@ contract L2LiquidityPool is CrossDomainEnabled, ReentrancyGuardUpgradeable, Paus external payable whenNotPaused + onlyWithBillingContract { - uint256 startingGas = gasleft(); - require(startingGas > extraGasRelay, "Insufficient Gas For a Relay Transaction"); - - uint256 desiredGasLeft = startingGas.sub(extraGasRelay); - uint256 i; - while (gasleft() > desiredGasLeft) { - i++; - } + // Collect the exit fee + L2BillingContract billingContract = L2BillingContract(billingContractAddress); + IERC20(BOBAAddress).safeTransferFrom(msg.sender, billingContractAddress, billingContract.exitFee()); require(msg.value != 0 || _tokenAddress != Lib_PredeployAddresses.OVM_ETH, "Either Amount Incorrect or Token Address Incorrect"); // combine to make logical XOR to avoid user error diff --git a/packages/boba/contracts/deploy/015-ExitBurn.deploy.ts b/packages/boba/contracts/deploy/015-ExitBurn.deploy.ts deleted file mode 100644 index d2b87eec10..0000000000 --- a/packages/boba/contracts/deploy/015-ExitBurn.deploy.ts +++ /dev/null @@ -1,50 +0,0 @@ -/* Imports: External */ -import { getContractFactory } from '@eth-optimism/contracts' -import { DeployFunction, DeploymentSubmission } from 'hardhat-deploy/dist/types' -import { Contract, ContractFactory, utils, BigNumber } from 'ethers' -import { registerBobaAddress } from './000-Messenger.deploy' - -import DiscretionaryExitBurnJson from '../artifacts/contracts/DiscretionaryExitBurn.sol/DiscretionaryExitBurn.json' - -let Factory__DiscretionaryExitBurn: ContractFactory -let DiscretionaryExitBurn: Contract - -const deployFn: DeployFunction = async (hre) => { - const addressManager = getContractFactory('Lib_AddressManager') - .connect((hre as any).deployConfig.deployer_l1) - .attach(process.env.ADDRESS_MANAGER_ADDRESS) as any - - Factory__DiscretionaryExitBurn = new ContractFactory( - DiscretionaryExitBurnJson.abi, - DiscretionaryExitBurnJson.bytecode, - (hre as any).deployConfig.deployer_l2 - ) - DiscretionaryExitBurn = await Factory__DiscretionaryExitBurn.deploy( - (hre as any).deployConfig.L2StandardBridgeAddress - ) - await DiscretionaryExitBurn.deployTransaction.wait() - console.log( - `DiscretionaryExitBurn deployed to: ${DiscretionaryExitBurn.address}` - ) - - const DiscretionaryExitBurnSubmission: DeploymentSubmission = { - ...DiscretionaryExitBurn, - receipt: DiscretionaryExitBurn.receipt, - address: DiscretionaryExitBurn.address, - abi: DiscretionaryExitBurn.abi, - } - - await hre.deployments.save( - 'DiscretionaryExitBurn', - DiscretionaryExitBurnSubmission - ) - await registerBobaAddress( - addressManager, - 'DiscretionaryExitBurn', - DiscretionaryExitBurn.address - ) -} - -deployFn.tags = ['ExitBurn'] - -export default deployFn diff --git a/packages/boba/contracts/deploy/015-ExitFee.deploy.ts b/packages/boba/contracts/deploy/015-ExitFee.deploy.ts new file mode 100644 index 0000000000..dedd18a552 --- /dev/null +++ b/packages/boba/contracts/deploy/015-ExitFee.deploy.ts @@ -0,0 +1,52 @@ +/* Imports: External */ +import { getContractFactory } from '@eth-optimism/contracts' +import { DeployFunction, DeploymentSubmission } from 'hardhat-deploy/dist/types' +import { Contract, ContractFactory, utils, BigNumber } from 'ethers' +import { registerBobaAddress } from './000-Messenger.deploy' + +import DiscretionaryExitFeeJson from '../artifacts/contracts/DiscretionaryExitFee.sol/DiscretionaryExitFee.json' + +let Factory__DiscretionaryExitFee: ContractFactory +let DiscretionaryExitFee: Contract + +const deployFn: DeployFunction = async (hre) => { + const addressManager = getContractFactory('Lib_AddressManager') + .connect((hre as any).deployConfig.deployer_l1) + .attach(process.env.ADDRESS_MANAGER_ADDRESS) as any + const L2BOBA = await hre.deployments.getOrNull('TK_L2BOBA') + + Factory__DiscretionaryExitFee = new ContractFactory( + DiscretionaryExitFeeJson.abi, + DiscretionaryExitFeeJson.bytecode, + (hre as any).deployConfig.deployer_l2 + ) + DiscretionaryExitFee = await Factory__DiscretionaryExitFee.deploy( + (hre as any).deployConfig.L2StandardBridgeAddress, + L2BOBA.address + ) + await DiscretionaryExitFee.deployTransaction.wait() + console.log( + `DiscretionaryExitFee deployed to: ${DiscretionaryExitFee.address}` + ) + + const DiscretionaryExitFeeSubmission: DeploymentSubmission = { + ...DiscretionaryExitFee, + receipt: DiscretionaryExitFee.receipt, + address: DiscretionaryExitFee.address, + abi: DiscretionaryExitFee.abi, + } + + await hre.deployments.save( + 'DiscretionaryExitFee', + DiscretionaryExitFeeSubmission + ) + await registerBobaAddress( + addressManager, + 'DiscretionaryExitFee', + DiscretionaryExitFee.address + ) +} + +deployFn.tags = ['ExitBurn'] + +export default deployFn diff --git a/packages/boba/contracts/deploy/019-L2BillingContract.deploy.ts b/packages/boba/contracts/deploy/019-L2BillingContract.deploy.ts new file mode 100644 index 0000000000..ba92ec8b8e --- /dev/null +++ b/packages/boba/contracts/deploy/019-L2BillingContract.deploy.ts @@ -0,0 +1,133 @@ +/* Imports: External */ +import { DeployFunction, DeploymentSubmission } from 'hardhat-deploy/dist/types' +import { Contract, ContractFactory, ethers } from 'ethers' +import { getContractFactory } from '@eth-optimism/contracts' +import { registerBobaAddress } from './000-Messenger.deploy' + +import ProxyJson from '../artifacts/contracts/libraries/Lib_ResolvedDelegateProxy.sol/Lib_ResolvedDelegateProxy.json' +import L2LiquidityPoolJson from '../artifacts/contracts/LP/L2LiquidityPool.sol/L2LiquidityPool.json' +import L2BillingContractJson from '../artifacts/contracts/L2BillingContract.sol/L2BillingContract.json' +import DiscretionaryExitFeeJson from '../artifacts/contracts/DiscretionaryExitFee.sol/DiscretionaryExitFee.json' + +let Factory__Proxy__L2BillingContract: ContractFactory +let Factory__L2BillingContract: ContractFactory + +let Proxy__L2LiquidityPool: Contract +let Proxy__L2BillingContract: Contract +let L2BillingContract: Contract +let DiscretionaryExitFee: Contract + +const deployFn: DeployFunction = async (hre) => { + const addressManager = getContractFactory('Lib_AddressManager') + .connect((hre as any).deployConfig.deployer_l1) + .attach(process.env.ADDRESS_MANAGER_ADDRESS) as any + + Factory__Proxy__L2BillingContract = new ContractFactory( + ProxyJson.abi, + ProxyJson.bytecode, + (hre as any).deployConfig.deployer_l2 + ) + + Factory__L2BillingContract = new ContractFactory( + L2BillingContractJson.abi, + L2BillingContractJson.bytecode, + (hre as any).deployConfig.deployer_l2 + ) + + console.log(`'Deploying L2 billing contract...`) + + const Proxy__L2LiquidityPoolDeployment = await hre.deployments.getOrNull( + 'Proxy__L2LiquidityPool' + ) + Proxy__L2LiquidityPool = new Contract( + Proxy__L2LiquidityPoolDeployment.address, + L2LiquidityPoolJson.abi, + (hre as any).deployConfig.deployer_l2 + ) + + L2BillingContract = await Factory__L2BillingContract.deploy() + await L2BillingContract.deployTransaction.wait() + const L2BillingContractDeploymentSubmission: DeploymentSubmission = { + ...L2BillingContract, + receipt: L2BillingContract.receipt, + address: L2BillingContract.address, + abi: L2BillingContract.abi, + } + await hre.deployments.save( + 'BobaBillingContract', + L2BillingContractDeploymentSubmission + ) + console.log(`BobaBillingContract deployed to: ${L2BillingContract.address}`) + + Proxy__L2BillingContract = await Factory__Proxy__L2BillingContract.deploy( + L2BillingContract.address + ) + await Proxy__L2BillingContract.deployTransaction.wait() + const Proxy__L2BillingContractDeploymentSubmission: DeploymentSubmission = { + ...Proxy__L2BillingContract, + receipt: Proxy__L2BillingContract.receipt, + address: Proxy__L2BillingContract.address, + abi: Proxy__L2BillingContract.abi, + } + await hre.deployments.save( + 'Proxy__BobaBillingContract', + Proxy__L2BillingContractDeploymentSubmission + ) + console.log( + `Proxy__BobaBillingContract deployed to: ${Proxy__L2BillingContract.address}` + ) + + // Initialize the billing contract + const L2BOBA = await hre.deployments.getOrNull('TK_L2BOBA') + Proxy__L2BillingContract = new Contract( + Proxy__L2BillingContract.address, + L2BillingContractJson.abi, + (hre as any).deployConfig.deployer_l2 + ) + await Proxy__L2BillingContract.initialize( + L2BOBA.address, + (hre as any).deployConfig.deployer_l2.address, + ethers.utils.parseEther('10') + ) + await L2BillingContract.initialize( + L2BOBA.address, + (hre as any).deployConfig.deployer_l2.address, + ethers.utils.parseEther('10') + ) + console.log(`Proxy__BobaBillingContract initialized`) + + // Register the address of the L2 billing contract + await Proxy__L2LiquidityPool.configureBillingContractAddress( + Proxy__L2BillingContract.address + ) + console.log(`Added BobaBillingContract to Proxy__L2LiquidityPool`) + + // Register the address of the L2 billing contract + const DiscretionaryExitFeeSubmission = await hre.deployments.getOrNull( + 'DiscretionaryExitFee' + ) + DiscretionaryExitFee = new Contract( + DiscretionaryExitFeeSubmission.address, + DiscretionaryExitFeeJson.abi, + (hre as any).deployConfig.deployer_l2 + ) + await DiscretionaryExitFee.configureBillingContractAddress( + Proxy__L2BillingContract.address + ) + console.log(`Added BobaBillingContract to DiscretionaryExitFee`) + + await registerBobaAddress( + addressManager, + 'Proxy__BobaBillingContract', + Proxy__L2BillingContract.address + ) + await registerBobaAddress( + addressManager, + 'BobaBillingContract', + L2BillingContract.address + ) +} + +deployFn.tags = ['Proxy__L1LiquidityPool', 'Proxy__L2LiquidityPool', 'required'] + +export default deployFn diff --git a/packages/boba/contracts/deploy/019-Dump.deploy.ts b/packages/boba/contracts/deploy/020-Dump.deploy.ts similarity index 100% rename from packages/boba/contracts/deploy/019-Dump.deploy.ts rename to packages/boba/contracts/deploy/020-Dump.deploy.ts From 2fb6bb9c030456a8edd6c4ca956b17027a6bced6 Mon Sep 17 00:00:00 2001 From: cby3149 Date: Thu, 7 Apr 2022 15:56:39 -0700 Subject: [PATCH 2/6] Add exit fee in L2 NFT bridge --- integration-tests/test/mrf_lp.spec.ts | 18 + integration-tests/test/nft_bridge.spec.ts | 553 ++++++++++++++---- .../test/routed_exit_fee.spec.ts | 21 +- .../contracts/DiscretionaryExitFee.sol | 6 +- .../contracts/LP/L2LiquidityPool.sol | 17 +- .../contracts/bridges/L2NFTBridge.sol | 41 +- .../contracts/deploy/015-ExitFee.deploy.ts | 4 +- .../deploy/019-L2BillingContract.deploy.ts | 16 + 8 files changed, 509 insertions(+), 167 deletions(-) diff --git a/integration-tests/test/mrf_lp.spec.ts b/integration-tests/test/mrf_lp.spec.ts index c9b2762f6d..df3393b052 100644 --- a/integration-tests/test/mrf_lp.spec.ts +++ b/integration-tests/test/mrf_lp.spec.ts @@ -2772,4 +2772,22 @@ describe('Liquidity Pool Test', async () => { // expect(preL2ERC20_2Balance).to.deep.eq(postL2ERC20_2Balance) }) }) + + describe('Configuration tests', async () => { + it('{tag:mrf} should not allow to configure billing contract address for non-owner', async () => { + await expect( + L2LiquidityPool.connect(env.l2Wallet_2).configureBillingContractAddress( + env.addressesBOBA.Proxy__BobaBillingContract + ) + ).to.be.revertedWith('Caller is not the owner') + }) + + it('{tag:mrf} should not allow to configure billing contract address to zero address', async () => { + await expect( + L2LiquidityPool.connect(env.l2Wallet).configureBillingContractAddress( + ethers.constants.AddressZero + ) + ).to.be.revertedWith('Billing contract address cannot be zero') + }) + }) }) diff --git a/integration-tests/test/nft_bridge.spec.ts b/integration-tests/test/nft_bridge.spec.ts index ea08223f0f..150ea0d04c 100644 --- a/integration-tests/test/nft_bridge.spec.ts +++ b/integration-tests/test/nft_bridge.spec.ts @@ -17,6 +17,8 @@ import L2ERC721UniqueDataJson from '../artifacts/contracts/TestUniqueDataL2Stand import ERC721ExtraDataJson from '../artifacts/contracts/TestExtraDataERC721.sol/TestExtraDataERC721.json' import L1ERC721ExtraDataJson from '../artifacts/contracts/TestExtraDataL1StandardERC721.sol/TestExtraDataL1StandardERC721.json' import L2ERC721ExtraDataJson from '../artifacts/contracts/TestExtraDataL2StandardERC721.sol/TestExtraDataL2StandardERC721.json' +import L2BillingContractJson from '@boba/contracts/artifacts/contracts/L2BillingContract.sol/L2BillingContract.json' +import L2GovernanceERC20Json from '@boba/contracts/artifacts/contracts/standards/L2GovernanceERC20.sol/L2GovernanceERC20.json' import { OptimismEnv } from './shared/env' import { ethers } from 'hardhat' @@ -29,6 +31,9 @@ describe('NFT Bridge Test', async () => { let L1ERC721: Contract let L2ERC721: Contract + let L2BOBAToken: Contract + let BOBABillingContract: Contract + let env: OptimismEnv const DUMMY_TOKEN_ID = 1234 @@ -49,6 +54,18 @@ describe('NFT Bridge Test', async () => { L2NFTBridge.abi, env.l2Wallet ) + + L2BOBAToken = new Contract( + env.addressesBOBA.TOKENS.BOBA.L2, + L2GovernanceERC20Json.abi, + env.l2Wallet + ) + + BOBABillingContract = new Contract( + env.addressesBOBA.Proxy__BobaBillingContract, + L2BillingContractJson.abi, + env.l2Wallet + ) }) describe('L1 native NFT tests', async () => { @@ -105,11 +122,7 @@ describe('NFT Bridge Test', async () => { await approveTx.wait() await env.waitForXDomainTransaction( - L1Bridge.depositNFT( - L1ERC721.address, - DUMMY_TOKEN_ID, - 9999999 - ) + L1Bridge.depositNFT(L1ERC721.address, DUMMY_TOKEN_ID, 9999999) ) const ownerL1 = await L1ERC721.ownerOf(DUMMY_TOKEN_ID) @@ -141,12 +154,51 @@ describe('NFT Bridge Test', async () => { ).to.be.reverted }) + it('{tag:boba} should fail to withdraw NFT if not enough Boba balance', async () => { + const newWallet = ethers.Wallet.createRandom().connect(env.l2Provider) + await env.l2Wallet.sendTransaction({ + to: newWallet.address, + value: ethers.utils.parseEther('1'), + }) + + await expect( + L2Bridge.connect(newWallet).withdraw( + L2ERC721.address, + DUMMY_TOKEN_ID, + 9999999 + ) + ).to.be.revertedWith( + 'execution reverted: ERC20: transfer amount exceeds balance' + ) + }) + + it('{tag:boba} should fail to withdraw NFT if not approving Boba', async () => { + await expect( + L2Bridge.connect(env.l2Wallet_2).withdraw( + L2ERC721.address, + DUMMY_TOKEN_ID, + 9999999 + ) + ).to.be.revertedWith( + 'execution reverted: ERC20: transfer amount exceeds allowance' + ) + }) + it('{tag:boba} should withdraw NFT', async () => { const approveTX = await L2ERC721.connect(env.l2Wallet_2).approve( L2Bridge.address, DUMMY_TOKEN_ID ) await approveTX.wait() + + // Approve BOBA + const exitFee = await BOBABillingContract.exitFee() + const approveBOBATX = await L2BOBAToken.connect(env.l2Wallet_2).approve( + L2Bridge.address, + exitFee + ) + await approveBOBATX.wait() + await env.waitForXDomainTransaction( L2Bridge.connect(env.l2Wallet_2).withdraw( L2ERC721.address, @@ -184,12 +236,53 @@ describe('NFT Bridge Test', async () => { expect(ownerL2).to.deep.eq(env.l2Wallet.address) }) + it('{tag:boba} should fail to withdraw NFT to another wallet if not enough Boba balance', async () => { + const newWallet = ethers.Wallet.createRandom().connect(env.l2Provider) + await env.l2Wallet.sendTransaction({ + to: newWallet.address, + value: ethers.utils.parseEther('1'), + }) + + await expect( + L2Bridge.connect(newWallet).withdrawTo( + L2ERC721.address, + env.l2Wallet_2.address, + DUMMY_TOKEN_ID, + 9999999 + ) + ).to.be.revertedWith( + 'execution reverted: ERC20: transfer amount exceeds balance' + ) + }) + + it('{tag:boba} should fail to withdraw NFT to another wallet if not approving Boba', async () => { + await expect( + L2Bridge.connect(env.l2Wallet_2).withdrawTo( + L2ERC721.address, + env.l2Wallet_2.address, + DUMMY_TOKEN_ID, + 9999999 + ) + ).to.be.revertedWith( + 'execution reverted: ERC20: transfer amount exceeds allowance' + ) + }) + it('{tag:boba} should withdraw NFT to another L1 wallet', async () => { const approveTX = await L2ERC721.connect(env.l2Wallet).approve( L2Bridge.address, DUMMY_TOKEN_ID ) await approveTX.wait() + + // Approve BOBA + const exitFee = await BOBABillingContract.exitFee() + const approveBOBATX = await L2BOBAToken.connect(env.l2Wallet).approve( + L2Bridge.address, + exitFee + ) + await approveBOBATX.wait() + await env.waitForXDomainTransaction( L2Bridge.connect(env.l2Wallet).withdrawTo( L2ERC721.address, @@ -243,12 +336,51 @@ describe('NFT Bridge Test', async () => { expect(ownerL2).to.deep.eq(env.l2Wallet_2.address) }) + it('{tag:boba} should fail to withdraw NFT with metadata if not enough Boba balance', async () => { + const newWallet = ethers.Wallet.createRandom().connect(env.l2Provider) + await env.l2Wallet.sendTransaction({ + to: newWallet.address, + value: ethers.utils.parseEther('1'), + }) + + await expect( + L2Bridge.connect(newWallet).withdrawWithExtraData( + L2ERC721.address, + DUMMY_TOKEN_ID, + 9999999 + ) + ).to.be.revertedWith( + 'execution reverted: ERC20: transfer amount exceeds balance' + ) + }) + + it('{tag:boba} should fail to withdraw NFT with metadata if not approving Boba', async () => { + await expect( + L2Bridge.connect(env.l2Wallet_2).withdrawWithExtraData( + L2ERC721.address, + DUMMY_TOKEN_ID, + 9999999 + ) + ).to.be.revertedWith( + 'execution reverted: ERC20: transfer amount exceeds allowance' + ) + }) + it('{tag:boba} should be able to attempt withdraw NFT with metadata', async () => { const approveTX = await L2ERC721.connect(env.l2Wallet_2).approve( L2Bridge.address, DUMMY_TOKEN_ID ) await approveTX.wait() + + // Approve BOBA + const exitFee = await BOBABillingContract.exitFee() + const approveBOBATX = await L2BOBAToken.connect(env.l2Wallet_2).approve( + L2Bridge.address, + exitFee + ) + await approveBOBATX.wait() + const withdrawTx = await env.waitForXDomainTransaction( L2Bridge.connect(env.l2Wallet_2).withdrawWithExtraData( L2ERC721.address, @@ -363,6 +495,43 @@ describe('NFT Bridge Test', async () => { await registerL2BridgeTx.wait() }) + it('{tag:boba} should fail to exit NFT if not enough Boba balance', async () => { + const newWallet = ethers.Wallet.createRandom().connect(env.l2Provider) + await env.l2Wallet.sendTransaction({ + to: newWallet.address, + value: ethers.utils.parseEther('1'), + }) + + await expect( + L2Bridge.connect(newWallet).withdraw( + L2ERC721.address, + DUMMY_TOKEN_ID, + 9999999 + ) + ).to.be.revertedWith( + 'execution reverted: ERC20: transfer amount exceeds balance' + ) + }) + + it('{tag:boba} should fail to exit NFT if not approving Boba', async () => { + // Reset allowance + const approveBOBATX = await L2BOBAToken.connect(env.l2Wallet_2).approve( + L2Bridge.address, + 0 + ) + await approveBOBATX.wait() + + await expect( + L2Bridge.connect(env.l2Wallet_2).withdraw( + L2ERC721.address, + DUMMY_TOKEN_ID, + 9999999 + ) + ).to.be.revertedWith( + 'execution reverted: ERC20: transfer amount exceeds allowance' + ) + }) + it('{tag:boba} should exit NFT from L2', async () => { // mint nft const mintTx = await L2ERC721.mint(env.l2Wallet.address, DUMMY_TOKEN_ID) @@ -371,12 +540,16 @@ describe('NFT Bridge Test', async () => { const approveTx = await L2ERC721.approve(L2Bridge.address, DUMMY_TOKEN_ID) await approveTx.wait() + // Approve BOBA + const exitFee = await BOBABillingContract.exitFee() + const approveBOBATX = await L2BOBAToken.connect(env.l2Wallet).approve( + L2Bridge.address, + exitFee + ) + await approveBOBATX.wait() + await env.waitForXDomainTransaction( - L2Bridge.withdraw( - L2ERC721.address, - DUMMY_TOKEN_ID, - 9999999 - ) + L2Bridge.withdraw(L2ERC721.address, DUMMY_TOKEN_ID, 9999999) ) const ownerL1 = await L1ERC721.ownerOf(DUMMY_TOKEN_ID) @@ -425,11 +598,7 @@ describe('NFT Bridge Test', async () => { await approveTx.wait() await env.waitForXDomainTransaction( - L1Bridge.depositNFT( - L1ERC721.address, - DUMMY_TOKEN_ID, - 9999999 - ) + L1Bridge.depositNFT(L1ERC721.address, DUMMY_TOKEN_ID, 9999999) ) await expect(L1ERC721.ownerOf(DUMMY_TOKEN_ID)).to.be.revertedWith( @@ -440,10 +609,57 @@ describe('NFT Bridge Test', async () => { expect(ownerL2).to.deep.eq(env.l2Wallet.address) }) + it('{tag:boba} should fail to exit NFT to another L1 wallet if not enough Boba balance', async () => { + const newWallet = ethers.Wallet.createRandom().connect(env.l2Provider) + await env.l2Wallet.sendTransaction({ + to: newWallet.address, + value: ethers.utils.parseEther('1'), + }) + + await expect( + L2Bridge.connect(newWallet).withdrawTo( + L2ERC721.address, + env.l2Wallet_2.address, + DUMMY_TOKEN_ID, + 9999999 + ) + ).to.be.revertedWith( + 'execution reverted: ERC20: transfer amount exceeds balance' + ) + }) + + it('{tag:boba} should fail to exit NFT to another L1 wallet if not approving Boba', async () => { + // Reset allowance + const approveBOBATX = await L2BOBAToken.connect(env.l2Wallet_2).approve( + L2Bridge.address, + 0 + ) + await approveBOBATX.wait() + + await expect( + L2Bridge.connect(env.l2Wallet_2).withdrawTo( + L2ERC721.address, + env.l2Wallet_2.address, + DUMMY_TOKEN_ID, + 9999999 + ) + ).to.be.revertedWith( + 'execution reverted: ERC20: transfer amount exceeds allowance' + ) + }) + it('{tag:boba} should exit NFT to another L1 wallet', async () => { const approveTx = await L2ERC721.approve(L2Bridge.address, DUMMY_TOKEN_ID) await approveTx.wait() + // Approve BOBA + const exitFee = await BOBABillingContract.exitFee() + const approveBOBATX = await L2BOBAToken.connect(env.l2Wallet).approve( + L2Bridge.address, + exitFee + ) + await approveBOBATX.wait() + await env.waitForXDomainTransaction( L2Bridge.withdrawTo( L2ERC721.address, @@ -467,6 +683,14 @@ describe('NFT Bridge Test', async () => { ) await approveTx.wait() + // Approve BOBA + const exitFee = await BOBABillingContract.exitFee() + const approveBOBATX = await L2BOBAToken.connect(env.l2Wallet_2).approve( + L2Bridge.address, + exitFee + ) + await approveBOBATX.wait() + await env.waitForXDomainTransaction( L1Bridge.connect(env.l1Wallet_2).depositNFTTo( L1ERC721.address, @@ -484,10 +708,55 @@ describe('NFT Bridge Test', async () => { expect(ownerL2).to.deep.eq(env.l2Wallet.address) }) + it('{tag:boba} should fail to exit NFT with metadata if not enough Boba balance', async () => { + const newWallet = ethers.Wallet.createRandom().connect(env.l2Provider) + await env.l2Wallet.sendTransaction({ + to: newWallet.address, + value: ethers.utils.parseEther('1'), + }) + + await expect( + L2Bridge.connect(newWallet).withdrawWithExtraData( + L2ERC721.address, + DUMMY_TOKEN_ID, + 9999999 + ) + ).to.be.revertedWith( + 'execution reverted: ERC20: transfer amount exceeds balance' + ) + }) + + it('{tag:boba} should fail to exit NFT with metadata if not approving Boba', async () => { + // Reset allowance + const approveBOBATX = await L2BOBAToken.connect(env.l2Wallet_2).approve( + L2Bridge.address, + 0 + ) + await approveBOBATX.wait() + + await expect( + L2Bridge.connect(env.l2Wallet_2).withdrawWithExtraData( + L2ERC721.address, + DUMMY_TOKEN_ID, + 9999999 + ) + ).to.be.revertedWith( + 'execution reverted: ERC20: transfer amount exceeds allowance' + ) + }) + it('{tag:boba} should be able to attempt exit NFT with metadata from L2', async () => { const approveTx = await L2ERC721.approve(L2Bridge.address, DUMMY_TOKEN_ID) await approveTx.wait() + // Approve BOBA + const exitFee = await BOBABillingContract.exitFee() + const approveBOBATX = await L2BOBAToken.connect(env.l2Wallet).approve( + L2Bridge.address, + exitFee + ) + await approveBOBATX.wait() + const withdrawTx = await env.waitForXDomainTransaction( L2Bridge.withdrawWithExtraData( L2ERC721.address, @@ -571,6 +840,14 @@ describe('NFT Bridge Test', async () => { ) await approveTx.wait() + // Approve BOBA + const exitFee = await BOBABillingContract.exitFee() + const approveBOBATX = await L2BOBAToken.connect(env.l2Wallet).approve( + L2Bridge.address, + exitFee + ) + await approveBOBATX.wait() + await expect( L2Bridge.withdraw(L2ERC721Test.address, DUMMY_TOKEN_ID, 9999999) ).to.be.revertedWith("Can't Find L1 NFT Contract") @@ -649,11 +926,7 @@ describe('NFT Bridge Test', async () => { await approveTx.wait() await env.waitForXDomainTransaction( - L1Bridge.depositNFT( - L1ERC721.address, - DUMMY_TOKEN_ID, - 9999999 - ) + L1Bridge.depositNFT(L1ERC721.address, DUMMY_TOKEN_ID, 9999999) ) }) @@ -663,6 +936,15 @@ describe('NFT Bridge Test', async () => { true ) await approveTX.wait() + + // Approve BOBA + const exitFee = await BOBABillingContract.exitFee() + const approveBOBATX = await L2BOBAToken.connect(env.l2Wallet_2).approve( + L2Bridge.address, + exitFee + ) + await approveBOBATX.wait() + await env.waitForXDomainTransaction( L2Bridge.connect(env.l2Wallet_2).withdraw( L2ERC721.address, @@ -729,12 +1011,16 @@ describe('NFT Bridge Test', async () => { const approveTx = await L2ERC721.approve(L2Bridge.address, DUMMY_TOKEN_ID) await approveTx.wait() + // Approve BOBA + const exitFee = await BOBABillingContract.exitFee() + const approveBOBATX = await L2BOBAToken.connect(env.l2Wallet).approve( + L2Bridge.address, + exitFee + ) + await approveBOBATX.wait() + await env.waitForXDomainTransaction( - L2Bridge.withdraw( - L2ERC721.address, - DUMMY_TOKEN_ID, - 9999999 - ) + L2Bridge.withdraw(L2ERC721.address, DUMMY_TOKEN_ID, 9999999) ) }) @@ -861,13 +1147,21 @@ describe('NFT Bridge Test', async () => { ) await approveTX.wait() + // Approve BOBA + const exitFee = await BOBABillingContract.exitFee() + const approveBOBATX = await L2BOBAToken.connect(env.l2Wallet).approve( + L2Bridge.address, + exitFee + ) + await approveBOBATX.wait() + // withdraw with metadata does not provide any advantage for non-native token const withdrawTx = await env.waitForXDomainTransaction( L2Bridge.connect(env.l2Wallet).withdrawWithExtraData( L2ERC721.address, DUMMY_TOKEN_ID, 9999999 - ) + ) ) // check event WithdrawalInitiated is emitted with empty data @@ -944,6 +1238,15 @@ describe('NFT Bridge Test', async () => { DUMMY_TOKEN_ID ) await approveTX.wait() + + // Approve BOBA + const exitFee = await BOBABillingContract.exitFee() + const approveBOBATX = await L2BOBAToken.connect(env.l2Wallet_2).approve( + L2Bridge.address, + exitFee + ) + await approveBOBATX.wait() + await env.waitForXDomainTransaction( L2Bridge.connect(env.l2Wallet_2).withdrawWithExtraDataTo( L2ERC721.address, @@ -1002,6 +1305,14 @@ describe('NFT Bridge Test', async () => { ) await approveTX.wait() + // Approve BOBA + const exitFee = await BOBABillingContract.exitFee() + const approveBOBATX = await L2BOBAToken.connect(env.l2Wallet).approve( + L2Bridge.address, + exitFee + ) + await approveBOBATX.wait() + // withdraw with metadata does not provide any advantage for non-native token const withdrawTx = await env.waitForXDomainTransaction( L2Bridge.connect(env.l2Wallet).withdrawWithExtraData( @@ -1147,6 +1458,14 @@ describe('NFT Bridge Test', async () => { ) await approveTX.wait() + // Approve BOBA + const exitFee = await BOBABillingContract.exitFee() + const approveBOBATX = await L2BOBAToken.connect(env.l2Wallet).approve( + L2Bridge.address, + exitFee + ) + await approveBOBATX.wait() + // withdraw with metadata does not provide any advantage for non-native token const withdrawTx = await env.waitForXDomainTransaction( L2Bridge.connect(env.l2Wallet).withdrawWithExtraData( @@ -1233,6 +1552,15 @@ describe('NFT Bridge Test', async () => { DUMMY_TOKEN_ID ) await approveTX.wait() + + // Approve BOBA + const exitFee = await BOBABillingContract.exitFee() + const approveBOBATX = await L2BOBAToken.connect(env.l2Wallet_2).approve( + L2Bridge.address, + exitFee + ) + await approveBOBATX.wait() + await env.waitForXDomainTransaction( L2Bridge.connect(env.l2Wallet_2).withdrawWithExtraDataTo( L2ERC721.address, @@ -1291,6 +1619,14 @@ describe('NFT Bridge Test', async () => { ) await approveTX.wait() + // Approve BOBA + const exitFee = await BOBABillingContract.exitFee() + const approveBOBATX = await L2BOBAToken.connect(env.l2Wallet).approve( + L2Bridge.address, + exitFee + ) + await approveBOBATX.wait() + // withdraw with metadata does not provide any advantage for non-native token const withdrawTx = await env.waitForXDomainTransaction( L2Bridge.connect(env.l2Wallet).withdrawWithExtraData( @@ -1381,6 +1717,14 @@ describe('NFT Bridge Test', async () => { const approveTx = await L2ERC721.approve(L2Bridge.address, DUMMY_TOKEN_ID) await approveTx.wait() + // Approve BOBA + const exitFee = await BOBABillingContract.exitFee() + const approveBOBATX = await L2BOBAToken.connect(env.l2Wallet).approve( + L2Bridge.address, + exitFee + ) + await approveBOBATX.wait() + const withdrawTx = await env.waitForXDomainTransaction( L2Bridge.withdrawWithExtraData( L2ERC721.address, @@ -1464,6 +1808,14 @@ describe('NFT Bridge Test', async () => { const approveTx = await L2ERC721.approve(L2Bridge.address, DUMMY_TOKEN_ID) await approveTx.wait() + // Approve BOBA + const exitFee = await BOBABillingContract.exitFee() + const approveBOBATX = await L2BOBAToken.connect(env.l2Wallet).approve( + L2Bridge.address, + exitFee + ) + await approveBOBATX.wait() + const withdrawTx = await env.waitForXDomainTransaction( L2Bridge.withdrawWithExtraDataTo( L2ERC721.address, @@ -1530,6 +1882,14 @@ describe('NFT Bridge Test', async () => { const approveTx = await L2ERC721.approve(L2Bridge.address, DUMMY_TOKEN_ID) await approveTx.wait() + // Approve BOBA + const exitFee = await BOBABillingContract.exitFee() + const approveBOBATX = await L2BOBAToken.connect(env.l2Wallet).approve( + L2Bridge.address, + exitFee + ) + await approveBOBATX.wait() + const withdrawTx = await env.waitForXDomainTransaction( L2Bridge.withdraw(L2ERC721.address, DUMMY_TOKEN_ID, 9999999) ) @@ -1659,6 +2019,14 @@ describe('NFT Bridge Test', async () => { const approveTx = await L2ERC721.approve(L2Bridge.address, DUMMY_TOKEN_ID) await approveTx.wait() + // Approve BOBA + const exitFee = await BOBABillingContract.exitFee() + const approveBOBATX = await L2BOBAToken.connect(env.l2Wallet).approve( + L2Bridge.address, + exitFee + ) + await approveBOBATX.wait() + const withdrawTx = await env.waitForXDomainTransaction( L2Bridge.withdrawWithExtraData( L2ERC721.address, @@ -1750,6 +2118,14 @@ describe('NFT Bridge Test', async () => { const approveTx = await L2ERC721.approve(L2Bridge.address, DUMMY_TOKEN_ID) await approveTx.wait() + // Approve BOBA + const exitFee = await BOBABillingContract.exitFee() + const approveBOBATX = await L2BOBAToken.connect(env.l2Wallet).approve( + L2Bridge.address, + exitFee + ) + await approveBOBATX.wait() + const withdrawTx = await env.waitForXDomainTransaction( L2Bridge.withdrawWithExtraDataTo( L2ERC721.address, @@ -1819,6 +2195,14 @@ describe('NFT Bridge Test', async () => { const approveTx = await L2ERC721.approve(L2Bridge.address, DUMMY_TOKEN_ID) await approveTx.wait() + // Approve BOBA + const exitFee = await BOBABillingContract.exitFee() + const approveBOBATX = await L2BOBAToken.connect(env.l2Wallet).approve( + L2Bridge.address, + exitFee + ) + await approveBOBATX.wait() + const withdrawTx = await env.waitForXDomainTransaction( L2Bridge.withdraw(L2ERC721.address, DUMMY_TOKEN_ID, 9999999) ) @@ -1995,6 +2379,14 @@ describe('NFT Bridge Test', async () => { const unpauseL2Tx = await L2Bridge.unpause() await unpauseL2Tx.wait() + // Approve BOBA + const exitFee = await BOBABillingContract.exitFee() + const approveBOBATX = await L2BOBAToken.connect(env.l2Wallet).approve( + L2Bridge.address, + exitFee + ) + await approveBOBATX.wait() + await env.waitForXDomainTransaction( L2Bridge.withdraw(L2ERC721.address, DUMMY_TOKEN_ID, 9999999) ) @@ -2024,108 +2416,21 @@ describe('NFT Bridge Test', async () => { }) }) - describe('Relay gas burn tests', async () => { - before(async () => { - Factory__L2ERC721 = new ContractFactory( - ERC721Json.abi, - ERC721Json.bytecode, - env.l2Wallet - ) - - Factory__L1ERC721 = new ContractFactory( - L1ERC721Json.abi, - L1ERC721Json.bytecode, - env.l1Wallet - ) - - // deploy a L2 native NFT token each time if existing contracts are used for tests - L2ERC721 = await Factory__L2ERC721.deploy('Test', 'TST') - - await L2ERC721.deployTransaction.wait() - - L1ERC721 = await Factory__L1ERC721.deploy( - L1Bridge.address, - L2ERC721.address, - 'Test', - 'TST', - '' // base-uri - ) - - await L1ERC721.deployTransaction.wait() - - // register NFT - const registerL1BridgeTx = await L1Bridge.registerNFTPair( - L1ERC721.address, - L2ERC721.address, - 'L2' - ) - await registerL1BridgeTx.wait() - - const registerL2BridgeTx = await L2Bridge.registerNFTPair( - L1ERC721.address, - L2ERC721.address, - 'L2' - ) - await registerL2BridgeTx.wait() - }) - - it('{tag:boba} should not allow updating extraGasRelay for non-owner', async () => { - const newExtraGasRelay = 500000 + describe('Configuration tests', async () => { + it('{tag:boba} should not allow to configure billing contract address for non-owner', async () => { await expect( - L2Bridge.connect(env.l2Wallet_2).configureExtraGasRelay( - newExtraGasRelay + L2Bridge.connect(env.l2Wallet_2).configureBillingContractAddress( + env.addressesBOBA.Proxy__BobaBillingContract ) - ).to.be.revertedWith('Caller is not the gasPriceOracle owner') - }) - - it('{tag:boba} should allow updating extraGasRelay for owner', async () => { - const mintTx = await L2ERC721.mint(env.l2Wallet.address, DUMMY_TOKEN_ID) - await mintTx.wait() - const approveTx = await L2ERC721.approve(L2Bridge.address, DUMMY_TOKEN_ID) - await approveTx.wait() - - const estimatedGas = await L2Bridge.estimateGas.withdraw( - L2ERC721.address, - DUMMY_TOKEN_ID, - 9999999 - ) - - const newExtraGasRelay = estimatedGas.mul(2) - const configureTx = await L2Bridge.connect( - env.l2Wallet_4 - ).configureExtraGasRelay(newExtraGasRelay) - await configureTx.wait() - - const updatedExtraGasRelay = await L2Bridge.extraGasRelay() - expect(updatedExtraGasRelay).to.eq(newExtraGasRelay) + ).to.be.revertedWith('Caller is not the owner') }) - it('{tag:boba} should be able to exit with the correct added gas', async () => { - const extraGas = 1000000 - - const resetGasTx = await L2Bridge.connect( - env.l2Wallet_4 - ).configureExtraGasRelay(0) - await resetGasTx.wait() - - const preGas = await L2Bridge.estimateGas.withdraw( - L2ERC721.address, - DUMMY_TOKEN_ID, - 9999999 - ) - - const addGasTx = await L2Bridge.connect( - env.l2Wallet_4 - ).configureExtraGasRelay(extraGas) - await addGasTx.wait() - - const afterGas = await L2Bridge.estimateGas.withdraw( - L2ERC721.address, - DUMMY_TOKEN_ID, - 9999999 - ) - - expect(afterGas).to.be.gt(preGas.add(BigNumber.from(extraGas))) + it('{tag:boba} should not allow to configure billing contract address to zero address', async () => { + await expect( + L2Bridge.connect(env.l2Wallet).configureBillingContractAddress( + ethers.constants.AddressZero + ) + ).to.be.revertedWith('Billing contract address cannot be zero') }) }) }) diff --git a/integration-tests/test/routed_exit_fee.spec.ts b/integration-tests/test/routed_exit_fee.spec.ts index e76965be84..d49de034b3 100644 --- a/integration-tests/test/routed_exit_fee.spec.ts +++ b/integration-tests/test/routed_exit_fee.spec.ts @@ -66,8 +66,7 @@ describe('Standard Exit Fee', async () => { ) ExitFeeContract = await Factory__ExitFeeContract.deploy( - L2StandardBridgeAddress, - env.addressesBOBA.TOKENS.BOBA.L2 + L2StandardBridgeAddress ) await ExitFeeContract.configureBillingContractAddress( @@ -365,4 +364,22 @@ describe('Standard Exit Fee', async () => { expect(postBobaBalance).to.eq(preBobaBalance.sub(exitFee)) }) }) + + describe('Configuration tests', async () => { + it('{tag:other} should not allow to configure billing contract address for non-owner', async () => { + await expect( + ExitFeeContract.connect(env.l2Wallet_2).configureBillingContractAddress( + env.addressesBOBA.Proxy__BobaBillingContract + ) + ).to.be.revertedWith('Ownable: caller is not the owner') + }) + + it('{tag:other} should not allow to configure billing contract address to zero address', async () => { + await expect( + ExitFeeContract.connect(env.l2Wallet).configureBillingContractAddress( + ethers.constants.AddressZero + ) + ).to.be.revertedWith('Billing contract address cannot be zero') + }) + }) }) diff --git a/packages/boba/contracts/contracts/DiscretionaryExitFee.sol b/packages/boba/contracts/contracts/DiscretionaryExitFee.sol index edc8c87cab..a5a8d557d9 100644 --- a/packages/boba/contracts/contracts/DiscretionaryExitFee.sol +++ b/packages/boba/contracts/contracts/DiscretionaryExitFee.sol @@ -16,12 +16,10 @@ contract DiscretionaryExitFee is Ownable { using SafeMath for uint256; address public l2Bridge; - address public BOBAAddress; address public billingContractAddress; - constructor(address _l2Bridge, address _BOBAAddress) { + constructor(address _l2Bridge) { l2Bridge = _l2Bridge; - BOBAAddress = _BOBAAddress; } modifier onlyWithBillingContract() { @@ -47,7 +45,7 @@ contract DiscretionaryExitFee is Ownable { ) external payable onlyWithBillingContract { // Collect the exit fee L2BillingContract billingContract = L2BillingContract(billingContractAddress); - IERC20(BOBAAddress).safeTransferFrom(msg.sender, billingContractAddress, billingContract.exitFee()); + IERC20(billingContract.feeTokenAddress()).safeTransferFrom(msg.sender, billingContractAddress, billingContract.exitFee()); require(msg.value != 0 || _l2Token != Lib_PredeployAddresses.OVM_ETH, "Amount Incorrect"); diff --git a/packages/boba/contracts/contracts/LP/L2LiquidityPool.sol b/packages/boba/contracts/contracts/LP/L2LiquidityPool.sol index 435ac9f975..e2155ccd98 100644 --- a/packages/boba/contracts/contracts/LP/L2LiquidityPool.sol +++ b/packages/boba/contracts/contracts/LP/L2LiquidityPool.sol @@ -206,11 +206,6 @@ contract L2LiquidityPool is CrossDomainEnabled, ReentrancyGuardUpgradeable, Paus _; } - modifier onlyGasPriceOracleOwner() { - require(msg.sender == OVM_GasPriceOracle(Lib_PredeployAddresses.OVM_GAS_PRICE_ORACLE).owner(), 'Caller is not the gasPriceOracle owner'); - _; - } - modifier onlyNotInitialized() { require(address(L1LiquidityPoolAddress) == address(0), "Contract has been initialized"); _; @@ -312,16 +307,6 @@ contract L2LiquidityPool is CrossDomainEnabled, ReentrancyGuardUpgradeable, Paus ownerRewardFeeRate = _ownerRewardFeeRate; } - function configureExtraGasRelay( - uint256 _extraGas - ) - public - onlyGasPriceOracleOwner() - onlyInitialized() - { - extraGasRelay = _extraGas; - } - /** * @dev Configure fee of the L1LP contract * @dev Each fee rate is scaled by 10^3 for precision, eg- a fee rate of 50 would mean 5% @@ -643,7 +628,7 @@ contract L2LiquidityPool is CrossDomainEnabled, ReentrancyGuardUpgradeable, Paus { // Collect the exit fee L2BillingContract billingContract = L2BillingContract(billingContractAddress); - IERC20(BOBAAddress).safeTransferFrom(msg.sender, billingContractAddress, billingContract.exitFee()); + IERC20(billingContract.feeTokenAddress()).safeTransferFrom(msg.sender, billingContractAddress, billingContract.exitFee()); require(msg.value != 0 || _tokenAddress != Lib_PredeployAddresses.OVM_ETH, "Either Amount Incorrect or Token Address Incorrect"); // combine to make logical XOR to avoid user error diff --git a/packages/boba/contracts/contracts/bridges/L2NFTBridge.sol b/packages/boba/contracts/contracts/bridges/L2NFTBridge.sol index 7fe9d33baf..8e9a43185b 100644 --- a/packages/boba/contracts/contracts/bridges/L2NFTBridge.sol +++ b/packages/boba/contracts/contracts/bridges/L2NFTBridge.sol @@ -24,6 +24,10 @@ import '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol'; import "@eth-optimism/contracts/contracts/L2/predeploys/OVM_GasPriceOracle.sol"; import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +/* External Imports */ +import { L2BillingContract } from "../L2BillingContract.sol"; /** * @title L2NFTBridge @@ -40,6 +44,7 @@ import "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol"; // add is interface contract L2NFTBridge is iL2NFTBridge, CrossDomainEnabled, ERC721Holder, ReentrancyGuardUpgradeable, PausableUpgradeable { using SafeMath for uint256; + using SafeERC20 for IERC20; /******************************** * External Contract References * @@ -64,6 +69,8 @@ contract L2NFTBridge is iL2NFTBridge, CrossDomainEnabled, ERC721Holder, Reentran // Maps L2 NFT address to NFTInfo mapping(address => PairNFTInfo) public pairNFTInfo; + // billing contract address + address public billingContractAddress; /*************** * Constructor * ***************/ @@ -80,13 +87,13 @@ contract L2NFTBridge is iL2NFTBridge, CrossDomainEnabled, ERC721Holder, Reentran _; } - modifier onlyGasPriceOracleOwner() { - require(msg.sender == OVM_GasPriceOracle(Lib_PredeployAddresses.OVM_GAS_PRICE_ORACLE).owner(), 'Caller is not the gasPriceOracle owner'); + modifier onlyInitialized() { + require(address(messenger) != address(0), "Contract has not yet been initialized"); _; } - modifier onlyInitialized() { - require(address(messenger) != address(0), "Contract has not yet been initialized"); + modifier onlyWithBillingContract() { + require(billingContractAddress != address(0), "Billing contract address is not set"); _; } @@ -144,16 +151,18 @@ contract L2NFTBridge is iL2NFTBridge, CrossDomainEnabled, ERC721Holder, Reentran } /** - * @param _extraGasRelay The extra gas for exiting L2 + * @dev Configure billing contract address. + * + * @param _billingContractAddress billing contract address */ - function configureExtraGasRelay( - uint256 _extraGasRelay + function configureBillingContractAddress( + address _billingContractAddress ) public - onlyGasPriceOracleOwner() - onlyInitialized() + onlyOwner() { - extraGasRelay = _extraGasRelay; + require(_billingContractAddress != address(0), "Billing contract address cannot be zero"); + billingContractAddress = _billingContractAddress; } /*** @@ -351,15 +360,11 @@ contract L2NFTBridge is iL2NFTBridge, CrossDomainEnabled, ERC721Holder, Reentran bytes memory _data ) internal + onlyWithBillingContract() { - uint256 startingGas = gasleft(); - require(startingGas > extraGasRelay, "Insufficient Gas For a Relay Transaction"); - - uint256 desiredGasLeft = startingGas.sub(extraGasRelay); - uint256 i; - while (gasleft() > desiredGasLeft) { - i++; - } + // Collect the exit fee + L2BillingContract billingContract = L2BillingContract(billingContractAddress); + IERC20(billingContract.feeTokenAddress()).safeTransferFrom(msg.sender, billingContractAddress, billingContract.exitFee()); PairNFTInfo storage pairNFT = pairNFTInfo[_l2Contract]; require(pairNFT.l1Contract != address(0), "Can't Find L1 NFT Contract"); diff --git a/packages/boba/contracts/deploy/015-ExitFee.deploy.ts b/packages/boba/contracts/deploy/015-ExitFee.deploy.ts index dedd18a552..a3b6e097ef 100644 --- a/packages/boba/contracts/deploy/015-ExitFee.deploy.ts +++ b/packages/boba/contracts/deploy/015-ExitFee.deploy.ts @@ -13,7 +13,6 @@ const deployFn: DeployFunction = async (hre) => { const addressManager = getContractFactory('Lib_AddressManager') .connect((hre as any).deployConfig.deployer_l1) .attach(process.env.ADDRESS_MANAGER_ADDRESS) as any - const L2BOBA = await hre.deployments.getOrNull('TK_L2BOBA') Factory__DiscretionaryExitFee = new ContractFactory( DiscretionaryExitFeeJson.abi, @@ -21,8 +20,7 @@ const deployFn: DeployFunction = async (hre) => { (hre as any).deployConfig.deployer_l2 ) DiscretionaryExitFee = await Factory__DiscretionaryExitFee.deploy( - (hre as any).deployConfig.L2StandardBridgeAddress, - L2BOBA.address + (hre as any).deployConfig.L2StandardBridgeAddress ) await DiscretionaryExitFee.deployTransaction.wait() console.log( diff --git a/packages/boba/contracts/deploy/019-L2BillingContract.deploy.ts b/packages/boba/contracts/deploy/019-L2BillingContract.deploy.ts index ba92ec8b8e..bd0c60d4ed 100644 --- a/packages/boba/contracts/deploy/019-L2BillingContract.deploy.ts +++ b/packages/boba/contracts/deploy/019-L2BillingContract.deploy.ts @@ -8,6 +8,7 @@ import ProxyJson from '../artifacts/contracts/libraries/Lib_ResolvedDelegateProx import L2LiquidityPoolJson from '../artifacts/contracts/LP/L2LiquidityPool.sol/L2LiquidityPool.json' import L2BillingContractJson from '../artifacts/contracts/L2BillingContract.sol/L2BillingContract.json' import DiscretionaryExitFeeJson from '../artifacts/contracts/DiscretionaryExitFee.sol/DiscretionaryExitFee.json' +import L2NFTBridgeJson from '../artifacts/contracts/bridges/L2NFTBridge.sol/L2NFTBridge.json' let Factory__Proxy__L2BillingContract: ContractFactory let Factory__L2BillingContract: ContractFactory @@ -16,6 +17,7 @@ let Proxy__L2LiquidityPool: Contract let Proxy__L2BillingContract: Contract let L2BillingContract: Contract let DiscretionaryExitFee: Contract +let L2NFTBridgeContract: Contract const deployFn: DeployFunction = async (hre) => { const addressManager = getContractFactory('Lib_AddressManager') @@ -116,6 +118,20 @@ const deployFn: DeployFunction = async (hre) => { ) console.log(`Added BobaBillingContract to DiscretionaryExitFee`) + // Register the address of the L2 NFT bridge + const Proxy__L2NFTBridgeSubmission = await hre.deployments.getOrNull( + 'Proxy__L2NFTBridge' + ) + L2NFTBridgeContract = new Contract( + Proxy__L2NFTBridgeSubmission.address, + L2NFTBridgeJson.abi, + (hre as any).deployConfig.deployer_l2 + ) + await L2NFTBridgeContract.configureBillingContractAddress( + Proxy__L2BillingContract.address + ) + console.log(`Added BobaBillingContract to Proxy__L2NFTBridgeSubmission`) + await registerBobaAddress( addressManager, 'Proxy__BobaBillingContract', From 05567f5f3b7fdbe75d748992827261971e30f2ab Mon Sep 17 00:00:00 2001 From: cby3149 Date: Thu, 7 Apr 2022 16:37:04 -0700 Subject: [PATCH 3/6] Update README --- .../contracts/contracts/bridges/README.md | 35 +++++++++++++------ 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/packages/boba/contracts/contracts/bridges/README.md b/packages/boba/contracts/contracts/bridges/README.md index 6f4c1c831c..4956fcaa0b 100644 --- a/packages/boba/contracts/contracts/bridges/README.md +++ b/packages/boba/contracts/contracts/bridges/README.md @@ -96,9 +96,15 @@ const approveTx = await L2NFT.approve(L2_NFT_BRIDGE_ADDRESS, TOKEN_ID) await approveTx.wait() ``` -Then, users call the `withdraw` or `withdrawTo` function to exit the NFT from Boba to Ethereum. The NFT will arrive on L1 after the seven days. +Users have to approve the Boba for the exit fee first. They then call the `withdraw` or `withdrawTo` function to exit the NFT from Boba to Ethereum. The NFT will arrive on L1 after the seven days. ```js +const exitFee = await BOBABillingContract.exitFee() +const approveBOBATX = await L2BOBAToken.connect(env.l2Wallet).approve( + L2Bridge.address, + exitFee +) +await approveBOBATX.wait() const tx = await L2NFTBrige.withdraw( L2_NFT_CONTRACT_ADDRESS, TOKEN_ID, @@ -116,9 +122,14 @@ const approveTx = await L2NFT.approve(L2_NFT_BRIDGE_ADDRESS, TOKEN_ID) await approveTx.wait() ``` -Users then call the `withdraw` or `withdrawTo` function to exit NFT from L2. The NFT will arrive on L1 after the seven days. +Users have to approve the Boba for the exit fee first. They then call the `withdraw` or `withdrawTo` function to exit NFT from L2. The NFT will arrive on L1 after the seven days. ```js +const exitFee = await BOBABillingContract.exitFee() +const approveBOBATX = await L2BOBAToken.connect(env.l2Wallet).approve( + L2Bridge.address, + exitFee +) const tx = await L2NFTBrige.withdraw( L2_NFT_CONTRACT_ADDRESS, TOKEN_ID, @@ -151,14 +162,18 @@ await tx.wait() ### Mainnet -| Contract Name | Contract Address | -| ------------------ | ------------------------------------------ | -| Proxy__L1NFTBridge | 0xC891F466e53f40603250837282eAE4e22aD5b088 | -| Proxy__L2NFTBridge | 0xFB823b65D0Dc219fdC0d759172D1E098dA32f9eb | +| Contract Name | Contract Address | +| -------------------------- | ------------------------------------------ | +| Proxy__L1NFTBridge | 0xC891F466e53f40603250837282eAE4e22aD5b088 | +| Proxy__L2NFTBridge | 0xFB823b65D0Dc219fdC0d759172D1E098dA32f9eb | +| Proxy__BobaBillingContract | 0x29F373e4869e69faaeCD3bF747dd1d965328b69f | +| TK_L2BOBA | 0xa18bF3994C0Cc6E3b63ac420308E5383f53120D7 | ### Rinkeby -| Contract Name | Contract Address | -| ------------------ | ------------------------------------------ | -| Proxy__L1NFTBridge | 0x01F5d5D6de3a8c7A157B22FD331A1F177b7bE043 | -| Proxy__L2NFTBridge | 0x5E368E9dce71B624D7DdB155f360E7A4969eB7aA | +| Contract Name | Contract Address | +| -------------------------- | ------------------------------------------ | +| Proxy__L1NFTBridge | 0x01F5d5D6de3a8c7A157B22FD331A1F177b7bE043 | +| Proxy__L2NFTBridge | 0x5E368E9dce71B624D7DdB155f360E7A4969eB7aA | +| Proxy__BobaBillingContract | 0x39ecF941443851762f58194e1eD54EE9F6987Cd1 | +| TK_L2BOBA | 0xF5B97a4860c1D81A1e915C40EcCB5E4a5E6b8309 | From 9d6c543455611cbcebd5865ccaacf8561835d264 Mon Sep 17 00:00:00 2001 From: cby3149 Date: Thu, 7 Apr 2022 16:41:36 -0700 Subject: [PATCH 4/6] Update README.md --- .../boba/contracts/contracts/bridges/README.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/boba/contracts/contracts/bridges/README.md b/packages/boba/contracts/contracts/bridges/README.md index 4956fcaa0b..8d6b16f848 100644 --- a/packages/boba/contracts/contracts/bridges/README.md +++ b/packages/boba/contracts/contracts/bridges/README.md @@ -96,15 +96,15 @@ const approveTx = await L2NFT.approve(L2_NFT_BRIDGE_ADDRESS, TOKEN_ID) await approveTx.wait() ``` -Users have to approve the Boba for the exit fee first. They then call the `withdraw` or `withdrawTo` function to exit the NFT from Boba to Ethereum. The NFT will arrive on L1 after the seven days. +Users have to approve the Boba for the exit fee next. They then call the `withdraw` or `withdrawTo` function to exit the NFT from Boba to Ethereum. The NFT will arrive on L1 after the seven days. ```js const exitFee = await BOBABillingContract.exitFee() -const approveBOBATX = await L2BOBAToken.connect(env.l2Wallet).approve( - L2Bridge.address, +const approveBOBATx = await L2BOBAToken.approve( + L2NFTBrige.address, exitFee ) -await approveBOBATX.wait() +await approveBOBATx.wait() const tx = await L2NFTBrige.withdraw( L2_NFT_CONTRACT_ADDRESS, TOKEN_ID, @@ -122,14 +122,15 @@ const approveTx = await L2NFT.approve(L2_NFT_BRIDGE_ADDRESS, TOKEN_ID) await approveTx.wait() ``` -Users have to approve the Boba for the exit fee first. They then call the `withdraw` or `withdrawTo` function to exit NFT from L2. The NFT will arrive on L1 after the seven days. +Users have to approve the Boba for the exit fee next. They then call the `withdraw` or `withdrawTo` function to exit NFT from L2. The NFT will arrive on L1 after the seven days. ```js const exitFee = await BOBABillingContract.exitFee() -const approveBOBATX = await L2BOBAToken.connect(env.l2Wallet).approve( - L2Bridge.address, +const approveBOBATx = await L2BOBAToken.approve( + L2NFTBrige.address, exitFee ) +await approveBOBATx.wait() const tx = await L2NFTBrige.withdraw( L2_NFT_CONTRACT_ADDRESS, TOKEN_ID, From 4405390021590e8f9794492ca5545fff5b97417a Mon Sep 17 00:00:00 2001 From: cby3149 Date: Fri, 8 Apr 2022 12:32:42 -0700 Subject: [PATCH 5/6] Record billing contract balance --- .../boba/gas-price-oracle/src/exec/run.ts | 11 - packages/boba/gas-price-oracle/src/service.ts | 404 ++++++------------ 2 files changed, 124 insertions(+), 291 deletions(-) diff --git a/packages/boba/gas-price-oracle/src/exec/run.ts b/packages/boba/gas-price-oracle/src/exec/run.ts index 587c5f3c71..93521a7690 100644 --- a/packages/boba/gas-price-oracle/src/exec/run.ts +++ b/packages/boba/gas-price-oracle/src/exec/run.ts @@ -63,15 +63,6 @@ const main = async () => { 'address-manager-address', env.ADDRESS_MANAGER_ADDRESS ) - // BURNED_GAS_FEE_RATIO_100X / 100 = ratio - const BURNED_GAS_FEE_RATIO_100X = config.uint( - 'burned-gas-fee-ratio-100x', - parseInt(env.BURNED_GAS_FEE_RATIO_100X, 10) || 30 - ) - const MAX_BURNED_GAS = config.str( - 'max-burned-gas', - env.MAX_BURNED_GAS || '10000000' - ) // OVERHEAD_RATIO_1000X / 1000 = ratio const OVERHEAD_RATIO_1000X = config.uint( 'overhead-ration-1000x', @@ -176,8 +167,6 @@ const main = async () => { relayerAddress, fastRelayerAddress, pollingInterval: POLLING_INTERVAL, - burnedGasFeeRatio100X: BURNED_GAS_FEE_RATIO_100X, - maxBurnedGas: MAX_BURNED_GAS, overheadRatio1000X: OVERHEAD_RATIO_1000X, overheadMinPercentChange: OVERHEAD_MIN_PERCENT_CHANGE, minOverhead: MIN_OVERHEAD, diff --git a/packages/boba/gas-price-oracle/src/service.ts b/packages/boba/gas-price-oracle/src/service.ts index 11b6db3754..ef53814161 100644 --- a/packages/boba/gas-price-oracle/src/service.ts +++ b/packages/boba/gas-price-oracle/src/service.ts @@ -43,12 +43,6 @@ interface GasPriceOracleOptions { // Interval in seconds to wait between loops pollingInterval: number - // Burned gas fee ratio - burnedGasFeeRatio100X: number - - // max burned gas - maxBurnedGas: string - // overhead ratio overheadRatio1000X: number @@ -93,13 +87,18 @@ export class GasPriceOracleService extends BaseService { Proxy__L1NFTBridge: Contract Proxy__L2NFTBridge: Contract Boba_GasPriceOracle: Contract + BobaBillingContractAddress: string L2BOBA: Contract L1ETHBalance: BigNumber L1ETHCostFee: BigNumber + L1RelayerBalance: BigNumber + L1RelayerCostFee: BigNumber L2ETHVaultBalance: BigNumber L2ETHCollectFee: BigNumber L2BOBAVaultBalance: BigNumber L2BOBACollectFee: BigNumber + L2BOBABillingBalance: BigNumber + L2BOBABillingCollectFee: BigNumber BOBAUSDPrice: number ETHUSDPrice: number } @@ -114,8 +113,6 @@ export class GasPriceOracleService extends BaseService { relayerWallet: this.options.relayerAddress, fastRelayerWallet: this.options.fastRelayerAddress, pollingInterval: this.options.pollingInterval, - burnedGasFeeRatio100X: this.options.burnedGasFeeRatio100X, - maxBurnedGas: this.options.maxBurnedGas, overheadRatio1000X: this.options.overheadRatio1000X, overheadMinPercentChange: this.options.overheadMinPercentChange, minOverhead: this.options.minOverhead, @@ -270,10 +267,29 @@ export class GasPriceOracleService extends BaseService { address: this.state.L2BOBA.address, }) + this.logger.info('Connecting to Proxy__BobaBillingContract...') + this.state.BobaBillingContractAddress = + await this.state.Lib_AddressManager.getAddress( + 'Proxy__BobaBillingContract' + ) + this.logger.info('Connected to Proxy__BobaBillingContract', { + address: this.state.BobaBillingContractAddress, + }) + + // Total cost this.state.L1ETHBalance = BigNumber.from('0') this.state.L1ETHCostFee = BigNumber.from('0') + // For ajusting the billing price + this.state.L1RelayerBalance = BigNumber.from('0') + this.state.L1RelayerCostFee = BigNumber.from('0') + // Total ETH revenuse this.state.L2ETHCollectFee = BigNumber.from('0') this.state.L2ETHVaultBalance = BigNumber.from('0') + // BOBA revenue + this.state.L2BOBAVaultBalance = BigNumber.from('0') + this.state.L2BOBACollectFee = BigNumber.from('0') + this.state.L2BOBABillingBalance = BigNumber.from('0') + this.state.L2BOBABillingCollectFee = BigNumber.from('0') // Load history await this._loadL1ETHFee() @@ -290,10 +306,6 @@ export class GasPriceOracleService extends BaseService { await this._getL1Balance() await this._getL2GasCost() await this._updatePriceRatio() - // extra burn gas - await this._updateFastExitGasBurnFee() - await this._updateClassicalExitGasBurnFee() - await this._updateNFTBridgeGasBurnFee() // l1 gas price and overhead fee await this._updateOverhead() await this._upateL1BaseFee() @@ -309,6 +321,12 @@ export class GasPriceOracleService extends BaseService { if (historyJSON.L1ETHCostFee) { this.state.L1ETHBalance = BigNumber.from(historyJSON.L1ETHBalance) this.state.L1ETHCostFee = BigNumber.from(historyJSON.L1ETHCostFee) + this.state.L1RelayerBalance = BigNumber.from( + historyJSON.L1RelayerBalance + ) + this.state.L1RelayerCostFee = BigNumber.from( + historyJSON.L1RelayerCostFee + ) } else { this.logger.warn('Invalid L1 cost history!') } @@ -328,6 +346,9 @@ export class GasPriceOracleService extends BaseService { const BOBAVaultBalance = await this.state.L2BOBA.balanceOf( this.state.Boba_GasPriceOracle.address ) + const BOBABillingBalance = await this.state.L2BOBA.balanceOf( + this.state.BobaBillingContractAddress + ) // load data const dumpsPath = path.resolve(__dirname, '../data/l2History.json') if (fs.existsSync(dumpsPath)) { @@ -350,10 +371,20 @@ export class GasPriceOracleService extends BaseService { this.logger.warn('Invalid L2 BOBA cost history!') this.state.L2BOBACollectFee = BOBAVaultBalance } + // Load Boba billing + if (historyJSON.L2BOBABillingCollectFee) { + this.state.L2BOBABillingCollectFee = BigNumber.from( + historyJSON.L2BOBABillingCollectFee + ) + } else { + this.logger.warn('Invalid L2 BOBA billing history!') + this.state.L2BOBABillingCollectFee = BOBABillingBalance + } } else { this.logger.warn('No L2 cost history Found!') this.state.L2ETHCollectFee = ETHVaultBalance this.state.L2BOBACollectFee = BOBAVaultBalance + this.state.L2BOBABillingCollectFee = BOBABillingBalance } // adjust the L2ETHCollectFee if it is not correct if (this.state.L2ETHCollectFee.lt(ETHVaultBalance)) { @@ -363,6 +394,10 @@ export class GasPriceOracleService extends BaseService { if (this.state.L2BOBACollectFee.lt(BOBAVaultBalance)) { this.state.L2BOBACollectFee = BOBAVaultBalance } + // adjust the L2BOBABillingCollectFee if it is not correct + if (this.state.L2BOBABillingCollectFee.lt(BOBABillingBalance)) { + this.state.L2BOBABillingCollectFee = BOBABillingBalance + } this.state.L2ETHVaultBalance = ETHVaultBalance this.state.L2BOBAVaultBalance = BOBAVaultBalance this.logger.info('Loaded L2 Cost Data', { @@ -370,6 +405,7 @@ export class GasPriceOracleService extends BaseService { L2ETHCollectFee: this.state.L2ETHCollectFee.toString(), L2BOBAVaultBalance: this.state.L2BOBAVaultBalance.toString(), L2BOBACollectFee: this.state.L2BOBACollectFee.toString(), + L2BOBABillingCollectFee: this.state.L2BOBABillingCollectFee.toString(), }) } @@ -385,6 +421,8 @@ export class GasPriceOracleService extends BaseService { JSON.stringify({ L1ETHBalance: this.state.L1ETHBalance.toString(), L1ETHCostFee: this.state.L1ETHCostFee.toString(), + L1RelayerBalance: this.state.L1RelayerBalance.toString(), + L1RelayerCostFee: this.state.L1RelayerCostFee.toString(), }) ) } catch (error) { @@ -405,6 +443,8 @@ export class GasPriceOracleService extends BaseService { JSON.stringify({ L2ETHCollectFee: this.state.L2ETHCollectFee.toString(), L2BOBACollectFee: this.state.L2BOBACollectFee.toString(), + L2BOBABillingCollectFee: + this.state.L2BOBABillingCollectFee.toString(), }) ) } catch (error) { @@ -441,6 +481,9 @@ export class GasPriceOracleService extends BaseService { return acc.add(cur) }, BigNumber.from('0')) + const L1RelayerETHBalanceLatest = balances[2].add(balances[3]) + + // ETH balance if (!this.state.L1ETHBalance.eq(BigNumber.from('0'))) { // condition 1 - L1ETHBalance <= L1ETHBalanceLatest -- do nothing // condition 2 - L1ETHBalance > L1ETHBalanceLatest @@ -460,7 +503,22 @@ export class GasPriceOracleService extends BaseService { ) } + // Relayer ETH balance + if (!this.state.L1RelayerBalance.eq(BigNumber.from('0'))) { + // condition 1 - L1RelayerBalance <= L1RelayerETHBalanceLatest -- do nothing + // condition 2 - L1RelayerBalance > L1RelayerETHBalanceLatest + if (this.state.L1RelayerBalance.gt(L1RelayerETHBalanceLatest)) { + this.state.L1RelayerCostFee = this.state.L1RelayerCostFee.add( + this.state.L1RelayerBalance.sub(L1RelayerETHBalanceLatest) + ) + } + } else { + // start from 0 + this.state.L1RelayerCostFee = BigNumber.from(0) + } + this.state.L1ETHBalance = L1ETHBalanceLatest + this.state.L1RelayerBalance = L1RelayerETHBalanceLatest // write history this._writeL1ETHFee() @@ -486,6 +544,20 @@ export class GasPriceOracleService extends BaseService { ) * this.state.ETHUSDPrice ).toFixed(2) ), + L1RelayerCostFee: Number( + Number( + utils.formatEther(this.state.L1RelayerCostFee.toString()) + ).toFixed(6) + ), + L1RelayerCostFeeUSD: Number( + ( + Number( + Number( + utils.formatEther(this.state.L1RelayerCostFee.toString()) + ) + ) * this.state.ETHUSDPrice + ).toFixed(2) + ), }, }) } catch (error) { @@ -518,7 +590,7 @@ export class GasPriceOracleService extends BaseService { L2ETHCollectFeeIncreased ) - // Get L2 BOBA Fee from contract + // Get L2 BOBA balance from contract const L2BOBACollectFee = await this.state.L2BOBA.balanceOf( this.state.Boba_GasPriceOracle.address ) @@ -537,6 +609,24 @@ export class GasPriceOracleService extends BaseService { L2BOBACollectFeeIncreased ) + // Get L2 BOBA Billing balance from contract + const L2BOBABillingCollectFee = await this.state.L2BOBA.balanceOf( + this.state.BobaBillingContractAddress + ) + // The BOBA in BobaBillingContract is zero after withdrawing it + let L2BOBABillingCollectFeeIncreased = BigNumber.from('0') + + if (L2BOBABillingCollectFee.lt(this.state.L2BOBABillingBalance)) { + this.state.L2BOBABillingBalance = L2BOBABillingCollectFee + } + L2BOBABillingCollectFeeIncreased = L2BOBABillingCollectFee.sub( + this.state.L2BOBABillingBalance + ) + this.state.L2BOBABillingBalance = L2BOBABillingCollectFee + + this.state.L2BOBABillingCollectFee = + this.state.L2BOBABillingCollectFee.add(L2BOBABillingCollectFeeIncreased) + await this._writeL2FeeCollect() this.logger.info('Got L2 Gas Collect', { @@ -565,6 +655,18 @@ export class GasPriceOracleService extends BaseService { ) * 10 ).toFixed(6) ), + L2BOBABillingCollectFee: Number( + Number( + utils.formatEther(this.state.L2BOBABillingCollectFee.toString()) + ).toFixed(6) + ), + L2BOBABillingCollectFee10X: Number( + ( + Number( + utils.formatEther(this.state.L2BOBABillingCollectFee.toString()) + ) * 10 + ).toFixed(6) + ), L2ETHCollectFeeUSD: Number( ( Number(utils.formatEther(this.state.L2ETHCollectFee.toString())) * @@ -578,6 +680,15 @@ export class GasPriceOracleService extends BaseService { ) * this.state.BOBAUSDPrice ).toFixed(2) ), + L2BOBABillingCollectFeeUSD: Number( + ( + Number( + utils.formatEther(this.state.L2BOBABillingCollectFee.toString()) + ) * this.state.BOBAUSDPrice + ).toFixed(2) + ), + BOBAUSDPrice: Number(this.state.BOBAUSDPrice.toFixed(2)), + ETHUSDPrice: Number(this.state.ETHUSDPrice.toFixed(2)), }, }) } catch (error) { @@ -637,182 +748,6 @@ export class GasPriceOracleService extends BaseService { } } - private async _updateFastExitGasBurnFee(): Promise { - try { - const latestL1Block = await this.options.l1RpcProvider.getBlockNumber() - const L1LiquidityPoolLog = - await this.state.Proxy__L1LiquidityPool.queryFilter( - this.state.Proxy__L1LiquidityPool.filters.ClientPayL1(), - Number(latestL1Block) - 10000, - Number(latestL1Block) - ) - const orderedL1LiquidityPoolLog = orderBy( - L1LiquidityPoolLog, - 'blockNumber', - 'desc' - ) - - // Calculate the average fee of last 50 relayed messages - let L1FastRelayerCost = BigNumber.from(0) - const transactionHashList = orderedL1LiquidityPoolLog.reduce( - (acc, cur, index) => { - if ( - index < 50 && - acc.length < 10 && - !acc.includes(cur.transactionHash) - ) { - acc.push(cur.transactionHash) - } - return acc - }, - [] - ) - const numberOfMessages = orderedL1LiquidityPoolLog.filter((i) => - transactionHashList.includes(i.transactionHash) - ).length - - if (numberOfMessages) { - for (const hash of transactionHashList) { - const txReceipt = - await this.options.l1RpcProvider.getTransactionReceipt(hash) - L1FastRelayerCost = L1FastRelayerCost.add( - txReceipt.effectiveGasPrice.mul(txReceipt.gasUsed) - ) - } - - const messageFee = L1FastRelayerCost.div( - BigNumber.from(numberOfMessages) - ) - const extraChargeFee = messageFee - .mul(BigNumber.from(this.options.burnedGasFeeRatio100X)) - .div(BigNumber.from(100)) - const L2GasPrice = await this.options.l2RpcProvider.getGasPrice() - const targetExtraGasRelay = extraChargeFee - .div(L2GasPrice) - .gt(BigNumber.from(this.options.maxBurnedGas)) - ? BigNumber.from(this.options.maxBurnedGas) - : extraChargeFee.div(L2GasPrice) - - const extraGasRelay = - await this.state.Proxy__L2LiquidityPool.extraGasRelay() - - if (targetExtraGasRelay.toString() === extraGasRelay.toString()) { - this.logger.info('No need to update extra gas', { - targetExtraGasRelay: targetExtraGasRelay.toNumber(), - extraGasRelay: extraGasRelay.toNumber(), - }) - } else { - this.logger.debug('Updating extra gas for Proxy__L2LiquidityPool...') - const tx = - await this.state.Proxy__L2LiquidityPool.configureExtraGasRelay( - targetExtraGasRelay, - { gasPrice: 0 } - ) - await tx.wait() - this.logger.info('Updated Proxy__L2LiquidityPool extra gas', { - extraGasRelay: targetExtraGasRelay.toNumber(), - }) - } - } else { - this.logger.info('No need to update extra gas for fast exit') - } - } catch (error) { - this.logger.warn(`CAN\'T UPDATE FAST EXIT BURNED RATIO ${error}`) - } - } - - private async _updateClassicalExitGasBurnFee(): Promise { - try { - const latestL1Block = await this.options.l1RpcProvider.getBlockNumber() - const L1StandardBridgeERC20Log = - await this.state.Proxy__L1StandardBridge.queryFilter( - this.state.Proxy__L1StandardBridge.filters.ERC20WithdrawalFinalized(), - Number(latestL1Block) - 10000, - Number(latestL1Block) - ) - const L1StandardBridgeETHLog = - await this.state.Proxy__L1StandardBridge.queryFilter( - this.state.Proxy__L1StandardBridge.filters.ETHWithdrawalFinalized(), - Number(latestL1Block) - 10000, - Number(latestL1Block) - ) - - const orderedL1StandardBridgeLog = orderBy( - [...L1StandardBridgeERC20Log, ...L1StandardBridgeETHLog], - 'blockNumber', - 'desc' - ) - - // Calculate the average fee of last 50 relayed messages - let L1ClassicalRelayerCost = BigNumber.from(0) - const transactionHashList = orderedL1StandardBridgeLog.reduce( - (acc, cur, index) => { - if ( - index < 50 && - acc.length < 10 && - !acc.includes(cur.transactionHash) - ) { - acc.push(cur.transactionHash) - } - return acc - }, - [] - ) - const numberOfMessages = [ - ...L1StandardBridgeERC20Log, - ...L1StandardBridgeETHLog, - ].filter((i) => transactionHashList.includes(i.transactionHash)).length - - if (numberOfMessages) { - for (const hash of transactionHashList) { - const txReceipt = - await this.options.l1RpcProvider.getTransactionReceipt(hash) - L1ClassicalRelayerCost = L1ClassicalRelayerCost.add( - txReceipt.effectiveGasPrice.mul(txReceipt.gasUsed) - ) - } - - const messageFee = L1ClassicalRelayerCost.div( - BigNumber.from(numberOfMessages) - ) - const extraChargeFee = messageFee - .mul(BigNumber.from(this.options.burnedGasFeeRatio100X)) - .div(BigNumber.from(100)) - const L2GasPrice = await this.options.l2RpcProvider.getGasPrice() - const targetExtraGasRelay = extraChargeFee - .div(L2GasPrice) - .gt(BigNumber.from(this.options.maxBurnedGas)) - ? BigNumber.from(this.options.maxBurnedGas) - : extraChargeFee.div(L2GasPrice) - - const extraGasRelay = - await this.state.DiscretionaryExitBurn.extraGasRelay() - - if (targetExtraGasRelay.toString() === extraGasRelay.toString()) { - this.logger.info('No need to update extra gas', { - targetExtraGasRelay: targetExtraGasRelay.toNumber(), - extraGasRelay: extraGasRelay.toNumber(), - }) - } else { - this.logger.debug('Updating extra gas for DiscretionaryExitBurn...') - const tx = - await this.state.DiscretionaryExitBurn.configureExtraGasRelay( - targetExtraGasRelay, - { gasPrice: 0 } - ) - await tx.wait() - this.logger.info('Updated DiscretionaryExitBurn extra gas', { - extraGasRelay: targetExtraGasRelay.toNumber(), - }) - } - } else { - this.logger.info('No need to update extra gas for classical exit') - } - } catch (error) { - this.logger.warn(`CAN\'T UPDATE CLASSICAL EXIT BURNED RATIO ${error}`) - } - } - private async _updateOverhead(): Promise { try { const latestL1Block = await this.options.l1RpcProvider.getBlockNumber() @@ -905,97 +840,6 @@ export class GasPriceOracleService extends BaseService { } } - private async _updateNFTBridgeGasBurnFee(): Promise { - try { - const latestL1Block = await this.options.l1RpcProvider.getBlockNumber() - const L1NFTBridgeSuccessLog = - await this.state.Proxy__L1NFTBridge.queryFilter( - this.state.Proxy__L1NFTBridge.filters.NFTWithdrawalFinalized(), - Number(latestL1Block) - 10000, - Number(latestL1Block) - ) - const L1NFTBridgeFailureLog = - await this.state.Proxy__L1NFTBridge.queryFilter( - this.state.Proxy__L1NFTBridge.filters.NFTWithdrawalFailed(), - Number(latestL1Block) - 10000, - Number(latestL1Block) - ) - - const orderedL1NFTBridgeLog = orderBy( - [...L1NFTBridgeSuccessLog, ...L1NFTBridgeFailureLog], - 'blockNumber', - 'desc' - ) - - // Calculate the average fee of last 50 relayed messages - let L1NFTRelayerCost = BigNumber.from(0) - const transactionHashList = orderedL1NFTBridgeLog.reduce( - (acc, cur, index) => { - if ( - index < 50 && - acc.length < 10 && - !acc.includes(cur.transactionHash) - ) { - acc.push(cur.transactionHash) - } - return acc - }, - [] - ) - const numberOfMessages = [ - ...L1NFTBridgeSuccessLog, - ...L1NFTBridgeFailureLog, - ].filter((i) => transactionHashList.includes(i.transactionHash)).length - - if (numberOfMessages) { - for (const hash of transactionHashList) { - const txReceipt = - await this.options.l1RpcProvider.getTransactionReceipt(hash) - L1NFTRelayerCost = L1NFTRelayerCost.add( - txReceipt.effectiveGasPrice.mul(txReceipt.gasUsed) - ) - } - - const messageFee = L1NFTRelayerCost.div( - BigNumber.from(numberOfMessages) - ) - const extraChargeFee = messageFee - .mul(BigNumber.from(this.options.burnedGasFeeRatio100X)) - .div(BigNumber.from(100)) - const L2GasPrice = await this.options.l2RpcProvider.getGasPrice() - const targetExtraGasRelay = extraChargeFee - .div(L2GasPrice) - .gt(BigNumber.from(this.options.maxBurnedGas)) - ? BigNumber.from(this.options.maxBurnedGas) - : extraChargeFee.div(L2GasPrice) - - const extraGasRelay = - await this.state.Proxy__L2NFTBridge.extraGasRelay() - - if (targetExtraGasRelay.toString() === extraGasRelay.toString()) { - this.logger.info('No need to update extra gas', { - targetExtraGasRelay: targetExtraGasRelay.toNumber(), - extraGasRelay: extraGasRelay.toNumber(), - }) - } else { - this.logger.debug('Updating extra gas for Proxy__L2NFTBridge...') - const tx = await this.state.Proxy__L2NFTBridge.configureExtraGasRelay( - targetExtraGasRelay, - { gasPrice: 0 } - ) - await tx.wait() - this.logger.info('Updated Proxy__L2NFTBridge extra gas', { - extraGasRelay: targetExtraGasRelay.toNumber(), - }) - } - } else { - this.logger.info('No need to update extra gas for NFT') - } - } catch (error) { - this.logger.warn(`CAN\'T UPDATE NFT EXIT BURNED RATIO ${error}`) - } - } - private async _upateL1BaseFee(): Promise { try { const l1GasPrice = await this.options.l1RpcProvider.getGasPrice() From 0ebe8ae5ad61d14af7410cdc01edb481469feb72 Mon Sep 17 00:00:00 2001 From: cby3149 Date: Tue, 12 Apr 2022 10:25:12 -0700 Subject: [PATCH 6/6] Update Rinkeby addresses --- packages/boba/contracts/contracts/DiscretionaryExitFee.sol | 2 +- ...esRinkeby_0x93A96D6A5beb1F661cf052722A1424CDDA3e9418.json | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/boba/contracts/contracts/DiscretionaryExitFee.sol b/packages/boba/contracts/contracts/DiscretionaryExitFee.sol index a5a8d557d9..b9dfb1b5ce 100644 --- a/packages/boba/contracts/contracts/DiscretionaryExitFee.sol +++ b/packages/boba/contracts/contracts/DiscretionaryExitFee.sol @@ -47,7 +47,7 @@ contract DiscretionaryExitFee is Ownable { L2BillingContract billingContract = L2BillingContract(billingContractAddress); IERC20(billingContract.feeTokenAddress()).safeTransferFrom(msg.sender, billingContractAddress, billingContract.exitFee()); - require(msg.value != 0 || _l2Token != Lib_PredeployAddresses.OVM_ETH, "Amount Incorrect"); + require(!(msg.value != 0 && _l2Token != Lib_PredeployAddresses.OVM_ETH), "Amount Incorrect"); if (msg.value != 0) { // override the _amount and token address diff --git a/packages/boba/register/addresses/addressesRinkeby_0x93A96D6A5beb1F661cf052722A1424CDDA3e9418.json b/packages/boba/register/addresses/addressesRinkeby_0x93A96D6A5beb1F661cf052722A1424CDDA3e9418.json index 75be8406a8..7ca5568222 100644 --- a/packages/boba/register/addresses/addressesRinkeby_0x93A96D6A5beb1F661cf052722A1424CDDA3e9418.json +++ b/packages/boba/register/addresses/addressesRinkeby_0x93A96D6A5beb1F661cf052722A1424CDDA3e9418.json @@ -50,7 +50,7 @@ "NFT_L2BOBO": "0x66C893019bC366eB497f49c8Df79e63AF73124eA", "L1CrossDomainMessengerFast": "0x7Af613eB9F72ecBdE81AE842c664949f43643eE8", "Proxy__L1CrossDomainMessengerFast": "0xe2a82CE9671A283190DD5E3f077027979F2c039E", - "L2LiquidityPool": "0x41561d87c7e32738bE5E4c9001c5dBC05e11cE31", + "L2LiquidityPool": "0x4E78194b4Aa7d0B467456458d70EC21099C466D1", "L1LiquidityPool": "0x0C16f3328aBDE8B353D4bF6B007261CE6C60d784", "Proxy__L1LiquidityPool": "0x12F8d1cD442cf1CF94417cE6309c6D2461Bd91a3", "Proxy__L2LiquidityPool": "0x56851CB42F315D0B90496c86E849167B8Cf7108a", @@ -61,7 +61,7 @@ "L2ERC721": "0xEdf91A2fBB745e4651eb57659929813bC3C01f14", "L2ERC721Reg": "0xf296469f3303760D20197C2a2c9Ab2aDe02bfDEB", "L1NFTBridge": "0x054DB39B7C859d1bCBfc156d3f7a5a2d00252115", - "L2NFTBridge": "0xB94bD836eBB6f3733e1a48534938224281fC3A9E", + "L2NFTBridge": "0x3e5A63009c18e27218851F4d4DB07c695A4274E0", "Proxy__L1NFTBridge": "0x01F5d5D6de3a8c7A157B22FD331A1F177b7bE043", "Proxy__L2NFTBridge": "0x5E368E9dce71B624D7DdB155f360E7A4969eB7aA", "L1MultiMessageRelayerFast": "0x94BC5F5330B9EF9f520551cDB6bD8FC707760Af6", @@ -69,6 +69,7 @@ "GovernorBravoDelegate": "0xBdD8Cb5D75F7375A9A45F40779e055596af7F581", "GovernorBravoDelegator": "0x472e5C097C790c6a44366a89a987Ec996A4d83e0", "DiscretionaryExitBurn": "0xF121Fd008A17c8C76DF1f003f19523130060B5BA", + "DiscretionaryExitFee": "0x583963636aa1344B9A609b34E1C9e7b5603df0A5", "BobaFixedSavings": "0x87668fD78F3a56Fe0bE323e03A09A120559942c4", "Proxy__BobaFixedSavings": "0x1fd7e7D032dBB3739791F8AaC2cA59073705F2aE", "BobaAirdropL1": "0x3067398621D1C1bfe10Fb4C321bF1005fa5BFc3E",