diff --git a/integration-tests/test/boba-fee-payment.spec.ts b/integration-tests/test/boba-fee-payment.spec.ts index 7d4bab822e..b1f13e320a 100644 --- a/integration-tests/test/boba-fee-payment.spec.ts +++ b/integration-tests/test/boba-fee-payment.spec.ts @@ -418,12 +418,12 @@ describe('Boba Fee Payment Integration Tests', async () => { }) it('{tag:boba} should not be able to withdraw fees before the minimum is met', async () => { - await expect(Boba_GasPriceOracle.withdraw()).to.be.rejected + await expect(Boba_GasPriceOracle.withdrawBOBA()).to.be.rejected }) it('{tag:boba} should be able to withdraw fees back to L1 once the minimum is met', async function () { - const l1FeeWallet = await Boba_GasPriceOracle.l1FeeWallet() - const balanceBefore = await L1Boba.balanceOf(l1FeeWallet) + const feeWallet = await Boba_GasPriceOracle.feeWallet() + const balanceBefore = await L1Boba.balanceOf(feeWallet) const withdrawalAmount = await Boba_GasPriceOracle.MIN_WITHDRAWAL_AMOUNT() const l2WalletBalance = await L2Boba.balanceOf(env.l2Wallet.address) @@ -446,7 +446,7 @@ describe('Boba Fee Payment Integration Tests', async () => { const vaultBalance = await L2Boba.balanceOf(Boba_GasPriceOracle.address) // Submit the withdrawal. - const withdrawTx = await Boba_GasPriceOracle.withdraw({ + const withdrawTx = await Boba_GasPriceOracle.withdrawBOBA({ gasPrice: 0, }) @@ -456,7 +456,7 @@ describe('Boba Fee Payment Integration Tests', async () => { await env.waitForXDomainTransaction(withdrawTx) // Balance difference should be equal to old L2 balance. - const balanceAfter = await L1Boba.balanceOf(l1FeeWallet) + const balanceAfter = await L1Boba.balanceOf(feeWallet) expect(balanceAfter.sub(balanceBefore)).to.deep.equal( BigNumber.from(vaultBalance) ) @@ -540,7 +540,7 @@ describe('Boba Fee Payment Integration Tests', async () => { while (priceRatio < 3000) { const setPriceRatio = await Boba_GasPriceOracle.connect( env.l2Wallet_4 - ).updatePriceRatio(priceRatio) + ).updatePriceRatio(priceRatio, priceRatio) await setPriceRatio.wait() const ETHBalanceBefore = await env.l2Wallet.getBalance() @@ -586,7 +586,7 @@ describe('Boba Fee Payment Integration Tests', async () => { while (priceRatio < 3000) { const setPriceRatio = await Boba_GasPriceOracle.connect( env.l2Wallet_4 - ).updatePriceRatio(priceRatio) + ).updatePriceRatio(priceRatio, priceRatio) await setPriceRatio.wait() const ETHBalanceBefore = await env.l2Wallet.getBalance() @@ -735,6 +735,12 @@ describe('Boba Fee Payment Integration Tests', async () => { }) await fundTx.wait() + const fundBobaTx = await L2Boba.transfer( + wallet.address, + ethers.utils.parseEther('10') + ) + await fundBobaTx.wait() + // Register the fee token const registerTx = await Boba_GasPriceOracle.connect( wallet @@ -779,12 +785,31 @@ describe('Boba Fee Payment Integration Tests', async () => { }) await fundTx.wait() + const fundBobaTx = await L2Boba.transfer( + wallet.address, + ethers.utils.parseEther('10') + ) + await fundBobaTx.wait() + // Register the fee token const registerTx = await Boba_GasPriceOracle.connect( wallet ).useBobaAsFeeToken() await registerTx.wait() + const BobaBalance = await L2Boba.balanceOf(wallet.address) + const estimateGas = await L2Boba.connect(wallet).estimateGas.transfer( + env.l2Wallet.address, + BobaBalance + ) + const priceRatio = await Boba_GasPriceOracle.priceRatio() + const returnBobaTx = await L2Boba.connect(wallet).transfer( + env.l2Wallet.address, + BobaBalance.sub(estimateGas.mul(priceRatio)), + { gasLimit: estimateGas } + ) + await returnBobaTx.wait() + await expect( wallet.sendTransaction({ to: env.l2Wallet.address, @@ -804,6 +829,12 @@ describe('Boba Fee Payment Integration Tests', async () => { }) await transferTx.wait() + const fundBobaTx = await L2Boba.transfer( + randomWallet.address, + ethers.utils.parseEther('10') + ) + await fundBobaTx.wait() + const registerTx = await Boba_GasPriceOracle.connect( randomWallet ).useBobaAsFeeToken() @@ -840,12 +871,19 @@ describe('Boba Fee Payment Integration Tests', async () => { name = await L2Boba.name() version = '1' chainId = (await env.l2Provider.getNetwork()).chainId + + // Add ETH first + await env.l2Wallet.sendTransaction({ + to: Boba_GasPriceOracle.address, + value: ethers.utils.parseEther('10'), + }) }) it('{tag:boba} should submit the meta transaction', async () => { const owner = env.l2Wallet_2.address const spender = Boba_GasPriceOracle.address - const value = (await Boba_GasPriceOracle.metaTransactionFee()).toString() + const receivedETHAmount = await Boba_GasPriceOracle.receivedETHAmount() + const value = (await Boba_GasPriceOracle.getBOBAForSwap()).toString() const nonce = (await L2Boba.nonces(env.l2Wallet_2.address)).toNumber() const deadline = Math.floor(Date.now() / 1000) + 90 const verifyingContract = L2Boba.address @@ -865,8 +903,12 @@ describe('Boba Fee Payment Integration Tests', async () => { const sig = ethers.utils.splitSignature(signature) const BobaBalanceBefore = await L2Boba.balanceOf(env.l2Wallet_2.address) + const ETHBalanceBefore = await env.l2Wallet_2.getBalance() + const GPO_ETHBalanceBefore = await env.l2Provider.getBalance( + Boba_GasPriceOracle.address + ) - await Boba_GasPriceOracle.useBobaAsFeeTokenMetaTransaction( + await Boba_GasPriceOracle.swapBOBAForETHMetaTransaction( owner, spender, value, @@ -876,21 +918,27 @@ describe('Boba Fee Payment Integration Tests', async () => { sig.s ) - const isBobaAsFeeToken = await Boba_GasPriceOracle.bobaFeeTokenUsers( - env.l2Wallet_2.address - ) const BobaBalanceAfter = await L2Boba.balanceOf(env.l2Wallet_2.address) + const ETHBalanceAfter = await env.l2Wallet_2.getBalance() + const GPO_ETHBalanceAfter = await env.l2Provider.getBalance( + Boba_GasPriceOracle.address + ) expect(BobaBalanceAfter).to.be.deep.eq( BobaBalanceBefore.sub(BigNumber.from(value)) ) - expect(isBobaAsFeeToken).to.be.eq(true) + expect(ETHBalanceAfter).to.be.deep.eq( + ETHBalanceBefore.add(receivedETHAmount) + ) + expect(GPO_ETHBalanceAfter).to.be.deep.eq( + GPO_ETHBalanceBefore.sub(receivedETHAmount) + ) }) it('{tag:boba} should revert transaction if v, r and s are incorrect', async () => { const owner = env.l2Wallet_2.address const spender = Boba_GasPriceOracle.address - const value = (await Boba_GasPriceOracle.metaTransactionFee()).toString() + const value = (await Boba_GasPriceOracle.getBOBAForSwap()).toString() const nonce = (await L2Boba.nonces(env.l2Wallet_2.address)).toNumber() const deadline = Math.floor(Date.now() / 1000) + 90 const verifyingContract = Boba_GasPriceOracle.address @@ -910,7 +958,7 @@ describe('Boba Fee Payment Integration Tests', async () => { const sig = ethers.utils.splitSignature(signature) await expect( - Boba_GasPriceOracle.useBobaAsFeeTokenMetaTransaction( + Boba_GasPriceOracle.swapBOBAForETHMetaTransaction( owner, spender, value, @@ -925,7 +973,7 @@ describe('Boba Fee Payment Integration Tests', async () => { it("{tag:boba} should revert transaction if users don't have sufficient Boba token", async () => { const owner = env.l2Wallet_2.address const spender = Boba_GasPriceOracle.address - const value = (await Boba_GasPriceOracle.metaTransactionFee()).toString() + const value = (await Boba_GasPriceOracle.getBOBAForSwap()).toString() const nonce = (await L2Boba.nonces(env.l2Wallet_2.address)).toNumber() const deadline = Math.floor(Date.now() / 1000) + 90 const verifyingContract = L2Boba.address @@ -959,7 +1007,9 @@ describe('Boba Fee Payment Integration Tests', async () => { await transferTx.wait() await expect( - Boba_GasPriceOracle.useBobaAsFeeTokenMetaTransaction( + Boba_GasPriceOracle.connect( + env.l2Wallet_2 + ).swapBOBAForETHMetaTransaction( owner, spender, value, @@ -982,7 +1032,7 @@ describe('Boba Fee Payment Integration Tests', async () => { it('{tag:boba} should revert transaction if spender is not correct', async () => { const owner = env.l2Wallet_2.address const spender = env.addressesBOBA.FeedRegistry - const value = (await Boba_GasPriceOracle.metaTransactionFee()).toString() + const value = (await Boba_GasPriceOracle.getBOBAForSwap()).toString() const nonce = (await L2Boba.nonces(env.l2Wallet_2.address)).toNumber() const deadline = Math.floor(Date.now() / 1000) + 90 const verifyingContract = L2Boba.address @@ -1002,7 +1052,7 @@ describe('Boba Fee Payment Integration Tests', async () => { const sig = ethers.utils.splitSignature(signature) await expect( - Boba_GasPriceOracle.useBobaAsFeeTokenMetaTransaction( + Boba_GasPriceOracle.swapBOBAForETHMetaTransaction( owner, spender, value, @@ -1037,7 +1087,7 @@ describe('Boba Fee Payment Integration Tests', async () => { const sig = ethers.utils.splitSignature(signature) await expect( - Boba_GasPriceOracle.useBobaAsFeeTokenMetaTransaction( + Boba_GasPriceOracle.swapBOBAForETHMetaTransaction( owner, spender, value, @@ -1048,5 +1098,101 @@ describe('Boba Fee Payment Integration Tests', async () => { ) ).to.be.revertedWith('Value is not enough') }) + + it('{tag:boba} should swap BOBA for ETH using Boba as the fee token', async () => { + const newWallet = ethers.Wallet.createRandom().connect(env.l2Provider) + + // Use Boba as the fee token + await env.l2Wallet.sendTransaction({ + to: newWallet.address, + value: ethers.utils.parseEther('1'), + }) + await L2Boba.transfer(newWallet.address, ethers.utils.parseEther('100')) + await Boba_GasPriceOracle.connect(newWallet).useBobaAsFeeToken() + + // Get BOBA + await L2Boba.transfer(newWallet.address, ethers.utils.parseEther('100')) + + // Transfer ETH back + const ETHBalance = await newWallet.getBalance() + await newWallet.sendTransaction({ + to: env.l2Wallet.address, + value: ETHBalance, + }) + + const BobaBalanceBefore = await L2Boba.balanceOf(newWallet.address) + const ETHBalanceBefore = await newWallet.getBalance() + const GPO_ETHBalanceBefore = await env.l2Provider.getBalance( + Boba_GasPriceOracle.address + ) + + const owner = newWallet.address + const spender = Boba_GasPriceOracle.address + const receivedETHAmount = await Boba_GasPriceOracle.receivedETHAmount() + const value = (await Boba_GasPriceOracle.getBOBAForSwap()).toString() + const nonce = (await L2Boba.nonces(newWallet.address)).toNumber() + const deadline = Math.floor(Date.now() / 1000) + 90 + const verifyingContract = L2Boba.address + + const data: any = { + primaryType: 'Permit', + types: { EIP712Domain, Permit }, + domain: { name, version, chainId, verifyingContract }, + message: { owner, spender, value, nonce, deadline }, + } + + const signature = ethSigUtil.signTypedData( + Buffer.from(newWallet.privateKey.slice(2), 'hex'), + { data } + ) + + const sig = ethers.utils.splitSignature(signature) + + await Boba_GasPriceOracle.swapBOBAForETHMetaTransaction( + owner, + spender, + value, + deadline, + sig.v, + sig.r, + sig.s + ) + + const BobaBalanceAfter = await L2Boba.balanceOf(newWallet.address) + const ETHBalanceAfter = await newWallet.getBalance() + const GPO_ETHBalanceAfter = await env.l2Provider.getBalance( + Boba_GasPriceOracle.address + ) + + expect(BobaBalanceAfter).to.be.deep.eq( + BobaBalanceBefore.sub(BigNumber.from(value)) + ) + expect(ETHBalanceAfter).to.be.deep.eq( + ETHBalanceBefore.add(receivedETHAmount) + ) + expect(GPO_ETHBalanceAfter).to.be.deep.eq( + GPO_ETHBalanceBefore.sub(receivedETHAmount) + ) + }) + + it('{tag:boba} should retrieve ETH', async () => { + const feeWallet = await Boba_GasPriceOracle.feeWallet() + const ETHBalanceBefore = await env.l2Provider.getBalance(feeWallet) + const GPO_ETHBalanceBefore = await env.l2Provider.getBalance( + Boba_GasPriceOracle.address + ) + + await Boba_GasPriceOracle.connect(env.l2Wallet_4).withdrawETH() + + const ETHBalanceAfter = await env.l2Provider.getBalance(feeWallet) + const GPO_ETHBalanceAfter = await env.l2Provider.getBalance( + Boba_GasPriceOracle.address + ) + + expect(ETHBalanceAfter).to.be.eq( + ETHBalanceBefore.add(GPO_ETHBalanceBefore) + ) + expect(GPO_ETHBalanceAfter).to.be.eq(BigNumber.from(0)) + }) }) }) diff --git a/ops_boba/api/metatransaction-api/metaTransaction_useBobaAsFeeToken.js b/ops_boba/api/metatransaction-api/metaTransaction_swapBOBAForETH.js similarity index 78% rename from ops_boba/api/metatransaction-api/metaTransaction_useBobaAsFeeToken.js rename to ops_boba/api/metatransaction-api/metaTransaction_swapBOBAForETH.js index 31505f04fc..05b4bf4b57 100644 --- a/ops_boba/api/metatransaction-api/metaTransaction_useBobaAsFeeToken.js +++ b/ops_boba/api/metatransaction-api/metaTransaction_swapBOBAForETH.js @@ -20,8 +20,10 @@ const BobaGasPriceOracleInterface = new ethers.utils.Interface([ 'function useBobaAsFeeToken()', 'function useETHAsFeeToken()', 'function bobaFeeTokenUsers(address) view returns (bool)', - 'function useBobaAsFeeTokenMetaTransaction(address,address,uint256,uint256,uint8,bytes32,bytes32)', + 'function swapBOBAForETHMetaTransaction(address,address,uint256,uint256,uint8,bytes32,bytes32)', 'function metaTransactionFee() view returns (uint256)', + 'function marketPriceRatio() view returns (uint256)', + 'function receivedETHAmount() view returns (uint256)', ]) const L2BobaInterface = new ethers.utils.Interface([ @@ -81,9 +83,14 @@ const verifyBobay = async (body) => { } const metaTransactionFee = await Boba_GasPriceOracle.metaTransactionFee() + const marketPriceRatio = await Boba_GasPriceOracle.marketPriceRatio() + const receivedETHAmount = await Boba_GasPriceOracle.receivedETHAmount() + const totalCost = receivedETHAmount + .mul(marketPriceRatio) + .add(metaTransactionFee) const L2BobaBalance = await L2Boba.balanceOf(owner) const bigNumberValue = ethers.BigNumber.from(value) - if (bigNumberValue.lt(metaTransactionFee)) { + if (bigNumberValue.lt(totalCost)) { return { isVerified: false, errorMessage: 'Invalid value', @@ -122,7 +129,7 @@ module.exports.mainnetHandler = async (event, context, callback) => { const sig = ethers.utils.splitSignature(signature) // Send transaction to node try { - const tx = await Boba_GasPriceOracle.useBobaAsFeeTokenMetaTransaction( + const tx = await Boba_GasPriceOracle.swapBOBAForETHMetaTransaction( owner, spender, value, @@ -163,9 +170,32 @@ module.exports.rinkebyHandler = async (event, context, callback) => { }) } + const { owner, spender, value, deadline, signature } = body + // Get r s v from signature + const sig = ethers.utils.splitSignature(signature) + // Send transaction to node + try { + const tx = await Boba_GasPriceOracle.swapBOBAForETHMetaTransaction( + owner, + spender, + value, + deadline, + sig.v, + sig.r, + sig.s + ) + await tx.wait() + } catch (err) { + return callback(null, { + headers, + statusCode: 400, + body: JSON.stringify({ status: 'failure', error: err }), + }) + } + return callback(null, { headers, - statusCode: 400, - body: JSON.stringify({ status: 'failure', error: 'not support' }), + statusCode: 201, + body: JSON.stringify({ status: 'success' }), }) } diff --git a/ops_boba/api/metatransaction-api/serverless-mainnet.yml b/ops_boba/api/metatransaction-api/serverless-mainnet.yml index 72f16ca513..cccee736e8 100644 --- a/ops_boba/api/metatransaction-api/serverless-mainnet.yml +++ b/ops_boba/api/metatransaction-api/serverless-mainnet.yml @@ -1,9 +1,9 @@ -service: sls-boba-mainnet-meta-transaction # NOTE: update this with your service name +service: sls-boba-mainnet-metaTransaction # NOTE: update this with your service name provider: name: aws runtime: nodejs12.x - stackName: sls-boba-mainnet-meta-transaction + stackName: sls-boba-mainnet-metaTransaction stage: prod region: us-east-1 role: ${file(env-mainnet.yml):ROLE} @@ -15,8 +15,8 @@ package: individually: true functions: - boba_useBobaAsFeeToken: - handler: metaTransaction_useBobaAsFeeToken.mainnetHandler + boba_swapBOBAForETH: + handler: metaTransaction_swapBOBAForETH.mainnetHandler memorySize: 10240 # optional, in MB, default is 1024 timeout: 60 # optional, in seconds, default is 6 vpc: @@ -27,7 +27,7 @@ functions: - ${file(env-mainnet.yml):SUBNET_ID_2} events: - http: - path: send.useBobaAsFeeToken + path: send.swapBOBAForETH method: post cors: true layers: diff --git a/ops_boba/api/metatransaction-api/serverless-rinkeby.yml b/ops_boba/api/metatransaction-api/serverless-rinkeby.yml index ae5efb41b8..2fafdfac1a 100644 --- a/ops_boba/api/metatransaction-api/serverless-rinkeby.yml +++ b/ops_boba/api/metatransaction-api/serverless-rinkeby.yml @@ -1,9 +1,9 @@ -service: sls-boba-rinkeby-meta-transaction # NOTE: update this with your service name +service: sls-boba-rinkeby-metaTransaction # NOTE: update this with your service name provider: name: aws runtime: nodejs12.x - stackName: sls-boba-rinkeby-meta-transaction + stackName: sls-boba-rinkeby-metaTransaction stage: prod region: us-east-1 role: ${file(env-rinkeby.yml):ROLE} @@ -15,8 +15,8 @@ package: individually: true functions: - boba_useBobaAsFeeToken: - handler: metaTransaction_useBobaAsFeeToken.rinkebyHandler + boba_swapBOBAForETH: + handler: metaTransaction_swapBOBAForETH.rinkebyHandler memorySize: 10240 # optional, in MB, default is 1024 timeout: 60 # optional, in seconds, default is 6 vpc: @@ -27,7 +27,7 @@ functions: - ${file(env-rinkeby.yml):SUBNET_ID_2} events: - http: - path: send.useBobaAsFeeToken + path: send.swapBOBAForETH method: post cors: true layers: diff --git a/packages/boba/gas-price-oracle/src/service.ts b/packages/boba/gas-price-oracle/src/service.ts index 11b6db3754..3d80f49fd6 100644 --- a/packages/boba/gas-price-oracle/src/service.ts +++ b/packages/boba/gas-price-oracle/src/service.ts @@ -597,6 +597,9 @@ export class GasPriceOracleService extends BaseService { this.options.bobaFeeRatio100X) / 100 ) + const targetMarketPriceRatio = Math.floor( + this.state.ETHUSDPrice / this.state.BOBAUSDPrice + ) if (targetPriceRatio !== priceRatioInt) { let targetUpdatedPriceRatio = targetPriceRatio if (targetPriceRatio > priceRatio) { @@ -618,11 +621,13 @@ export class GasPriceOracleService extends BaseService { const gasPriceTx = await this.state.Boba_GasPriceOracle.updatePriceRatio( targetUpdatedPriceRatio, + targetMarketPriceRatio, { gasPrice: 0 } ) await gasPriceTx.wait() this.logger.info('Updated price ratio', { priceRatio: targetUpdatedPriceRatio, + targetMarketPriceRatio, }) } else { this.logger.info('No need to update price ratio', { diff --git a/packages/boba/gateway/src/containers/wallet/Wallet.js b/packages/boba/gateway/src/containers/wallet/Wallet.js index e9535ee1e1..9968069451 100644 --- a/packages/boba/gateway/src/containers/wallet/Wallet.js +++ b/packages/boba/gateway/src/containers/wallet/Wallet.js @@ -100,11 +100,9 @@ function Wallet() { if (accountEnabled && l2Balances.length > 0) { const l2BalanceETH = l2Balances.find((i) => i.symbol === 'ETH') const l2BalanceBOBA = l2Balances.find((i) => i.symbol === 'BOBA') - if (l2BalanceETH && l2BalanceETH[0]) { setTooSmallETH(new BN(logAmount(l2BalanceETH[0].balance, 18)).lt(new BN(0.003))) } - if (l2BalanceBOBA && l2BalanceBOBA[0]) { setTooSmallBOBA(new BN(logAmount(l2BalanceBOBA[0].balance, 18)).lt(new BN(4.0))) } @@ -140,6 +138,9 @@ function Wallet() { if (res) dispatch(openAlert('Emergency Swap submitted')) } +// disable hisding the EMERGENCY SWAP for testing +// + return ( @@ -148,15 +149,18 @@ function Wallet() { - WARNING: Low ETH balance. + NOTE: ETH balance. {' '} Using Boba requires a minimum ETH balance (of 0.002 ETH) regardless of your fee setting, otherwise MetaMask may incorrectly reject transactions. - If you are stuck because you ran out of ETH, use EMERGENCY SWAP to swap BOBA for - 0.05 ETH at market rates. +

If you are stuck because you ran out of ETH, use EMERGENCY SWAP to swap BOBA for + 0.05 ETH at market rates. +

EMERGENCY SWAPs are metatransactions and are not shown in + the history tab, but can be looked up in the blockexplorer token transfers for BOBA. +