Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Immutable gas refunder #150

Open
wants to merge 1 commit into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
146 changes: 146 additions & 0 deletions src/bridge/GasRefunderOpt.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
// Copyright 2023-2024, Offchain Labs, Inc.
// For license information, see https://github.com/OffchainLabs/nitro-contracts/blob/main/LICENSE
// SPDX-License-Identifier: BUSL-1.1

pragma solidity ^0.8.7;

import "../libraries/IGasRefunder.sol";

import "@openzeppelin/contracts/access/Ownable.sol";

/**
* @notice Optimized version of GasRefunder intended especially for use by sequencer inbox batch posters.
* @dev This contract allows any refundee as long as the caller is the allowedContract.
*/
contract GasRefunderOpt is IGasRefunder, Ownable {
address public immutable allowedContract;
uint256 public immutable maxRefundeeBalance;
uint256 public immutable extraGasMargin;
uint256 public immutable calldataCost;
uint256 public immutable maxGasTip;
uint256 public immutable maxGasCost;
uint256 public immutable maxSingleGasUsage;

enum RefundDenyReason {
CONTRACT_NOT_ALLOWED,
REFUNDEE_NOT_ALLOWED,
REFUNDEE_ABOVE_MAX_BALANCE,
OUT_OF_FUNDS
}

event SuccessfulRefundedGasCosts(uint256 gas, uint256 gasPrice, uint256 amountPaid);

event FailedRefundedGasCosts(uint256 gas, uint256 gasPrice, uint256 amountPaid);

event RefundGasCostsDenied(
address indexed refundee,
address indexed contractAddress,
RefundDenyReason indexed reason,
uint256 gas
);
event Deposited(address sender, uint256 amount);
event Withdrawn(address initiator, address destination, uint256 amount);

constructor(
address _allowedContract,
uint256 _maxRefundeeBalance,
uint256 _extraGasMargin,
uint256 _calldataCost,
uint256 _maxGasTip,
uint256 _maxGasCost,
uint256 _maxSingleGasUsage
) Ownable() {
allowedContract = _allowedContract;
maxRefundeeBalance = _maxRefundeeBalance;
extraGasMargin = _extraGasMargin;
calldataCost = _calldataCost;
maxGasTip = _maxGasTip;
maxGasCost = _maxGasCost;
maxSingleGasUsage = _maxSingleGasUsage;
}

receive() external payable {
emit Deposited(msg.sender, msg.value);
}

function withdraw(address payable destination, uint256 amount) external onlyOwner {
// It's expected that destination is an EOA
(bool success, ) = destination.call{value: amount}("");
require(success, "WITHDRAW_FAILED");
emit Withdrawn(msg.sender, destination, amount);
}

function onGasSpent(
address payable refundee,
uint256 gasUsed,
uint256 calldataSize
) external override returns (bool success) {
uint256 startGasLeft = gasleft();

uint256 ownBalance = address(this).balance;

if (ownBalance == 0) {
emit RefundGasCostsDenied(refundee, msg.sender, RefundDenyReason.OUT_OF_FUNDS, gasUsed);
return false;
}

if (allowedContract != msg.sender) {
emit RefundGasCostsDenied(
refundee,
msg.sender,
RefundDenyReason.CONTRACT_NOT_ALLOWED,
gasUsed
);
return false;
}

uint256 estGasPrice = block.basefee + maxGasTip;
if (tx.gasprice < estGasPrice) {
estGasPrice = tx.gasprice;
}
if (maxGasCost != 0 && estGasPrice > maxGasCost) {
estGasPrice = maxGasCost;
}

uint256 refundeeBalance = refundee.balance;

// Add in a bit of a buffer for the tx costs not measured with gasleft
gasUsed += startGasLeft + extraGasMargin + (calldataSize * calldataCost);
// Split this up into two statements so that gasleft() comes after the extra arithmetic
gasUsed -= gasleft();

if (maxSingleGasUsage != 0 && gasUsed > maxSingleGasUsage) {
gasUsed = maxSingleGasUsage;
}

uint256 refundAmount = estGasPrice * gasUsed;
if (maxRefundeeBalance != 0 && refundeeBalance + refundAmount > maxRefundeeBalance) {
if (refundeeBalance > maxRefundeeBalance) {
// The refundee is already above their max balance
// emit RefundGasCostsDenied(
// refundee,
// msg.sender,
// RefundDenyReason.REFUNDEE_ABOVE_MAX_BALANCE,
// gasUsed
// );
return false;
} else {
refundAmount = maxRefundeeBalance - refundeeBalance;
}
}

if (refundAmount > ownBalance) {
refundAmount = ownBalance;
}

// It's expected that refundee is an EOA
// solhint-disable-next-line avoid-low-level-calls
(success, ) = refundee.call{value: refundAmount}("");

if (success) {
emit SuccessfulRefundedGasCosts(gasUsed, estGasPrice, refundAmount);
} else {
emit FailedRefundedGasCosts(gasUsed, estGasPrice, refundAmount);
}
}
}
133 changes: 133 additions & 0 deletions test/contract/sequencerInboxForceInclude.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
import {
Bridge,
Bridge__factory,
GasRefunder__factory,
GasRefunderOpt__factory,
Inbox,
Inbox__factory,
MessageTester,
Expand Down Expand Up @@ -246,6 +248,13 @@
const bridgeFac = (await ethers.getContractFactory(
'Bridge'
)) as Bridge__factory
const gasRefunderFac = (await ethers.getContractFactory(
'GasRefunder'
)) as GasRefunder__factory
const GasRefunderOptFac = (await ethers.getContractFactory(
'GasRefunderOpt'
)) as GasRefunderOpt__factory

const bridgeTemplate = await bridgeFac.deploy()
const transparentUpgradeableProxyFac = (await ethers.getContractFactory(
'TransparentUpgradeableProxy'
Expand All @@ -266,6 +275,7 @@
adminAddr,
'0x'
)

const bridge = await bridgeFac.attach(bridgeProxy.address).connect(user)
const bridgeAdmin = await bridgeFac
.attach(bridgeProxy.address)
Expand Down Expand Up @@ -295,6 +305,35 @@
await bridgeAdmin.setDelayedInbox(inbox.address, true)
await bridgeAdmin.setSequencerInbox(sequencerInbox.address)

const gasRefunder = await gasRefunderFac.deploy()
await gasRefunder.deployed()
await (await gasRefunder.allowContracts([sequencerInbox.address])).wait()
await (
await gasRefunder.allowRefundees([await batchPoster.getAddress()])
).wait()
await (await gasRefunder.setExtraGasMargin(35000)).wait()

const GasRefunderOpt = await GasRefunderOptFac.deploy(
sequencerInbox.address,
0, // no limit
35000, // extra gas margin
16, // gas per calldata byte
2e9, // maxGasTip 2 gwei
120e9, // maxGasCost 120 gwei
2e6 // maxSingleGasUsage 2 million gas
)
await GasRefunderOpt.deployed()

await accounts[0].sendTransaction({
value: ethers.utils.parseEther('10000.0'),
to: gasRefunder.address,
})

await accounts[0].sendTransaction({
value: ethers.utils.parseEther('10000.0'),
to: GasRefunderOpt.address,
})

await (
await sequencerInbox
.connect(rollupOwner)
Expand All @@ -317,6 +356,8 @@
bridgeProxy,
rollup,
rollupOwner,
GasRefunderOpt,
gasRefunder,
}
}

Expand Down Expand Up @@ -357,6 +398,98 @@
).wait()
})

it('can add batch and refund gas', async () => {
const {
user,
inbox,
bridge,
messageTester,
sequencerInbox,
batchPoster,
gasRefunder,
} = await setupSequencerInbox()

await sendDelayedTx(
user,
inbox,
bridge,
messageTester,
1000000,
21000000000,
0,
await user.getAddress(),
BigNumber.from(10),
'0x1010'
)
const messagesRead = await bridge.delayedMessageCount()
const seqReportedMessageSubCount =
await bridge.sequencerReportedSubMessageCount()
const balBefore = await batchPoster.getBalance()
const txn = await (

Check warning on line 428 in test/contract/sequencerInboxForceInclude.spec.ts

View workflow job for this annotation

GitHub Actions / Contract tests

'txn' is assigned a value but never used. Allowed unused vars must match /^_/u
await sequencerInbox
.connect(batchPoster)
.functions[
'addSequencerL2BatchFromOrigin(uint256,bytes,uint256,address,uint256,uint256)'
](
0,
data,
messagesRead,
gasRefunder.address,
seqReportedMessageSubCount,
seqReportedMessageSubCount.add(10),
{ gasLimit: 10000000 }
)
).wait()
// console.log('Mutable Gas Refunder', txn.gasUsed.toNumber())
expect((await batchPoster.getBalance()).gt(balBefore), 'Refund not enough')
})

it('can add batch and refund gas', async () => {
const {
user,
inbox,
bridge,
messageTester,
sequencerInbox,
batchPoster,
GasRefunderOpt,
} = await setupSequencerInbox()

await sendDelayedTx(
user,
inbox,
bridge,
messageTester,
1000000,
21000000000,
0,
await user.getAddress(),
BigNumber.from(10),
'0x1010'
)
const messagesRead = await bridge.delayedMessageCount()
const seqReportedMessageSubCount =
await bridge.sequencerReportedSubMessageCount()
const balBefore = await batchPoster.getBalance()
const txn = await (

Check warning on line 474 in test/contract/sequencerInboxForceInclude.spec.ts

View workflow job for this annotation

GitHub Actions / Contract tests

'txn' is assigned a value but never used. Allowed unused vars must match /^_/u
await sequencerInbox
.connect(batchPoster)
.functions[
'addSequencerL2BatchFromOrigin(uint256,bytes,uint256,address,uint256,uint256)'
](
0,
data,
messagesRead,
GasRefunderOpt.address,
seqReportedMessageSubCount,
seqReportedMessageSubCount.add(10),
{ gasLimit: 10000000 }
)
).wait()
// console.log('Sequencer Gas Refunder', txn.gasUsed.toNumber())
expect((await batchPoster.getBalance()).gt(balBefore), 'Refund not enough')
})

it('can force-include', async () => {
const { user, inbox, bridge, messageTester, sequencerInbox } =
await setupSequencerInbox()
Expand Down
Loading