Basic Operations Examples
Learn how to use basic features of Boba (e.g. bridges, basic L2 ops) through examples
To see examples of how to perform basic operations on Boba v2, please see the react code for the Boba gateway.
Below, we provide code snippets for several typical operations on the L2:
  1. 1.
    Check the Gas Price
  2. 2.
    Estimate the cost of a contract call
  3. 3.
    An L2->L2 transfer
  4. 4.
    A 'classic' bridging operation
Overall, note that from the perspective of solidity code and rpc calls, Boba OVM 2.0 is identical to mainchain in most aspects, so your experience (and code) from mainchain should carry over directly. The main practical differences center on Gas and on cross-chain bridging operations.

1. Check the Current Gas Price

The Gas Price on L2 changes every 30 seconds, with some smoothing to reduce sharp discontinuities in the price from one moment to the next. The maximum percentage change of the l2 gas price is 5% in the gas price oracle. Like on mainchain, the current gas price can be obtained via .getGasPrice():
1
this.L2Provider = new ethers.providers.StaticJsonRpcProvider('mainnet.boba.network')
2
3
const gasPrice = await this.L2Provider.getGasPrice()
4
5
console.log("Current gas price:", gasPrice )
6
//prints: Current gas price: BigNumber {_hex: '0x02540be400', _isBigNumber: true}
7
8
console.log("Current gas price", gasPrice.toString())
9
//prints: Current gas price: 10000000000
Copied!
Typical values are 3 to 10 Gwei.

2. Estimate the cost of a contract call

Like on mainchain, the cost of a L2 transaction is the product of the current gas price and the 'complexity' of the contract call, with some calls being much more expensive than others. The contract call complexity is quantified via the gas. For example, the cost of an approval on L2 is about 0.0004 ETH, or about $1.70 (Oct. 2021):
1
const L2ERC20Contract = new ethers.Contract(
2
currencyAddress,
3
L2ERC20Json.abi,
4
this.provider.getSigner()
5
)
6
7
//this is the key call - this results in a TX body that can be used
8
//by estimateGas(TX) to estimate the gas
9
const tx = await L2ERC20Contract.populateTransaction.approve(
10
allAddresses.L2LPAddress,
11
utils.parseEther('1.0')
12
)
13
14
const approvalGas_BN = await this.L2Provider.estimateGas(tx)
15
16
approvalCost_BN = approvalGas_BN.mul(gasPrice)
17
18
console.log("Current gas price", gasPrice.toString())
19
console.log("Approval gas:", approvalGas_BN.toString())
20
console.log("Approval cost in ETH:", utils.formatEther(approvalCost_BN))
21
22
//Current gas price: 10000000000
23
//Approval gas: 44138
24
//Approval cost in ETH: 0.00044138
Copied!
NOTE: The gas for a particular transaction depends both on the nature of the call (e.g. approve) and the call parameters, such as the amount (in this case, 1.0 ETH). A common source of reverted transactions is to mis-estimate the gas, such as by calling .estimateGas() with a TX generated for a different value.
1
Typical L2 gas values:
2
3
Approve 0 ETH: 24866
4
Approve 1 ETH: 44138
5
Fast Exit: 141698
Copied!
NOTE: Unlike on L1, on L2 there is no benefit to paying more - you just waste ETH. The sequencer operates in first in first serve, and transaction order is determined by the arrival time of your transaction, not by how much you are willing to pay.
NOTE: To protect users, overpaying by more than a 10% percent will also revert your transactions. The core gas price logic is as follows:
1
//Core l2 gas price code logic
2
3
fee := new(big.Int).Set(opts.ExpectedGasPrice)
4
5
// Allow for a downward buffer to protect against L1 gas price volatility
6
if opts.ThresholdDown != nil {
7
fee = mulByFloat(fee, opts.ThresholdDown)
8
}
9
10
// Protect the sequencer from being underpaid
11
// if user fee < expected fee, return error
12
if opts.UserGasPrice.Cmp(fee) == -1 {
13
return ErrGasPriceTooLow
14
}
15
16
// Protect users from overpaying by too much
17
if opts.ThresholdUp != nil {
18
// overpaying = user fee - expected fee
19
overpaying := new(big.Int).Sub(opts.UserGasPrice, opts.ExpectedGasPrice)
20
threshold := mulByFloat(opts.ExpectedGasPrice, opts.ThresholdUp)
21
// if overpaying > threshold, return error
22
if overpaying.Cmp(threshold) == 1 {
23
return ErrGasPriceTooHigh
24
}
25
}
Copied!
Gas Price tolerance band : The gasPrice you use should be within 10% of the value reported by .getGasPrice(). Let’s say the gasPrice is 100 Gwei. Then, the l2geth will accept any gasPrice between 90 Gwei to 110 Gwei.

3. An L2->L2 transfer

1
//Transfer funds from one account to another, on the L2
2
async transfer(address, value_Wei_String, currency) {
3
4
let tx = null
5
6
try {
7
8
if(currency === allAddresses.L2_ETH_Address) {
9
10
//we are transferring ETH - special call
11
let wei = BigNumber.from(value_Wei_String)
12
13
tx = await this.provider.send('eth_sendTransaction',
14
[
15
{
16
from: this.account,
17
to: address,
18
value: ethers.utils.hexlify(wei)
19
}
20
]
21
)
22
23
} else {
24
// we are transferring an ERC20...
25
tx = await this.STANDARD_ERC20_Contract
26
.attach(currency)
27
.transfer(
28
address,
29
value_Wei_String
30
)
31
await tx.wait()
32
}
33
34
return tx
35
} catch (error) {
36
console.log("Transfer error:", error)
37
return error
38
}
39
}
40
Copied!

4. An L1->L2 Classic Bridge Operation

1
//Move ERC20 Tokens from L1 to L2
2
async depositErc20(value_Wei_String, currency, currencyL2) {
3
4
const L1_TEST_Contract = this.L1_TEST_Contract.attach(currency)
5
6
let allowance_BN = await L1_TEST_Contract.allowance(
7
this.account,
8
allAddresses.L1StandardBridgeAddress
9
)
10
11
const allowed = allowance_BN.gte(BigNumber.from(value_Wei_String))
12
13
if(!allowed) {
14
const approveStatus = await L1_TEST_Contract.approve(
15
allAddresses.L1StandardBridgeAddress,
16
value_Wei_String
17
)
18
await approveStatus.wait()
19
console.log("ERC 20 L1 ops approved:",approveStatus)
20
}
21
22
const depositTxStatus = await this.L1StandardBridgeContract.depositERC20(
23
currency,
24
currencyL2,
25
value_Wei_String,
26
this.L2GasLimit,
27
utils.formatBytes32String(new Date().getTime().toString())
28
)
29
30
//at this point the tx has been submitted, and we are waiting...
31
await depositTxStatus.wait()
32
33
const [l1ToL2msgHash] = await this.watcher.getMessageHashesFromL1Tx(
34
depositTxStatus.hash
35
)
36
console.log(' got L1->L2 message hash', l1ToL2msgHash)
37
38
const l2Receipt = await this.watcher.getL2TransactionReceipt(
39
l1ToL2msgHash
40
)
41
console.log(' completed Deposit! L2 tx hash:', l2Receipt.transactionHash)
42
43
return l2Receipt
44
45
}
Copied!

5. Accessing latest L1 Block number

NOTICE
The hex value that corresponds to the L1BLOCKNUMBER opcode (0x4B) may be changed in the future (pending further discussion). We strongly discourage direct use of this opcode within your contracts. Instead, if you want to access the latest L1 block number, please use the OVM_L1BlockNumber contract as described below.
The block number of the latest L1 block seen by the L2 system can be accessed via the L1BLOCKNUMBER opcode. Solidity doesn't make it easy to use non-standard opcodes, so we've created a simple contract located at 0x4200000000000000000000000000000000000013 (opens new window)that will allow you to trigger this opcode.
You can use this contract as follows:
1
import { iOVM_L1BlockNumber } from "@eth-optimism/contracts/L2/predeploys/iOVM_L1BlockNumber.sol";
2
import { Lib_PredeployAddresses } from "@eth-optimism/contracts/libraries/constants/Lib_PredeployAddresses.sol";
3
4
contract MyContract {
5
function myFunction() public {
6
// ... your code here ...
7
8
uint256 l1BlockNumber = iOVM_L1BlockNumber(
9
Lib_PredeployAddresses.L1_BLOCK_NUMBER // located at 0x4200000000000000000000000000000000000013
10
).getL1BlockNumber();
11
12
// ... your code here ...
13
}
14
}
Copied!

Block Numbers and Timestamps

Block production is not constant

On Ethereum, the NUMBER opcode (block.number in Solidity) corresponds to the current Ethereum block number. Similarly, in Boba Network, block.number corresponds to the current L2 block number. However, as of the OVM 2.0 release of Optimistic Ethereum (Nov. 2021), each transaction on L2 is placed in a separate block and blocks are NOT produced at a constant rate.
This is important because it means that block.number is currently NOT a reliable source of timing information. If you want access to the current time, you should use block.timestamp (the TIMESTAMP opcode) instead.

Timestamp lags by up to 15 minutes

Note that block.timestamp is pulled automatically from the latest L1 block seen by the L2 system. L2 currently waits for about 15 minutes (~50 confirmations) before the L1 block is accepted. As a result, the timestamp may lag behind the current time by up to 15 minutes.
Last modified 1mo ago