Bond Incentives
Table of Contents
Overview
Bonds is an add-on to the core Fault Dispute Game. The core game mechanics are designed to ensure honesty as the best response to winning subgames. By introducing financial incentives, Bonds makes it worthwhile for honest challengers to participate. Without the bond reward incentive, the FDG will be too costly for honest players to participate in given the cost of verifying and making claims.
Implementations may allow the FDG to directly receive bonds, or delegate this responsibility to another entity. Regardless, there must be a way for the FDG to query and distribute bonds linked to a claim.
Bonds are integrated into the FDG in two areas:
- Moves
- Subgame Resolution
Moves
Moves must be adequately bonded to be added to the FDG. This document does not specify a scheme for determining the minimum bond requirement. FDG implementations should define a function computing the minimum bond requirement with the following signature:
function getRequiredBond(Position _movePosition) public pure returns (uint256 requiredBond_)
As such, attacking or defending requires a check for the getRequiredBond() amount against the bond
attached to the move. To incentivize participation, the minimum bond should cover the cost of a possible
counter to the move being added. Thus, the minimum bond depends only on the position of the move that's added.
Subgame Resolution
If a subgame root resolves incorrectly, then its bond is distributed to the leftmost claimant that countered it. This creates an incentive to identify the earliest point of disagreement in an execution trace. The subgame root claimant gets back its bond iff it resolves correctly.
At maximum game depths, where a claimant counters a bonded claim via step, the bond is instead distributed
to the account that successfully called step.
Leftmost Claim Incentives
There exists defensive positions that cannot be countered, even if they hold invalid claims. These positions are located on the same level as honest claims, but situated to its right (i.e. its gindex > honest claim's).
An honest challenger can always successfully dispute any sibling claims not positioned to the right of an honest claim. The leftmost payoff rule encourages such disputes, ensuring only one claim is leftmost at correct depths. This claim will be the honest one, and thus bond rewards will be directed exclusively to honest claims.
Fault Proof Mainnet Incentives
This section describes the specific bond incentives to be used for the Fault Proof Mainnet launch of the OP Stack fault proof system.
Authenticated Roles
| Name | Description |
|---|---|
| Guardian | Role responsible for blacklisting dispute game contracts and changing the respected dispute game type |
| System Owner | Role that owns the ProxyAdmin contract that in turn owns most Proxy contracts within the OP Stack |
Base Fee Assumption
FPM bonds are to assume a fixed 200 Gwei base fee.
Future iterations of the fault proof may include a dynamic base fee calculation.
For the moment, we suppose that the Guardian address may account for increased average base fees by updating the
OptimismPortal contract to a new respected game type with a higher assumed base fee.
Bond Scaling
FPM bonds are priced in the amount of gas that they are intended to cover.
Bonds start at the very first depth of the game at a baseline of 400_000 gas.
The 400_000 value is chosen as a deterrence amount that is approximately double the cost to respond at the top level.
Bonds scale up to a value of 300_000_000 gas, a value chosen to cover approximately double the cost of a max-size
Large Preimage Proposal.
We use a multiplicative scaling mechanism to guarantee that the ratio between bonds remains constant.
We determine the multiplier based on the proposed MAX_DEPTH of 73.
We can use the formula x = (300_000_000 / 400_000) ** (1 / 73) to determine that x = 1.09493.
At each depth N, the amount of gas charged is therefore 400_000 * (1.09493 ** N)
Below is a diagram demonstrating this curve for a max depth of 73.
Required Bond Formula
Applying the Base Fee Assumption and Bond Scaling specifications, we have a
getRequiredBond function:
def get_required_bond(position):
assumed_gas_price = 200 gwei
base_gas_charged = 400_000
gas_charged = 400_000 * (1.09493 ** position.depth)
return gas_charged * assumed_gas_price
Other Incentives
There are other costs associated with participating in the game, including operating a challenger agent and the opportunity cost of locking up capital in the dispute game. While we do not explicitly create incentives to cover these costs, we assume that the current bond rewards, based on this specification, are enough as a whole to cover all other costs of participation.
Game Finalization
After the game is resolved, claimants must wait for the AnchorStateRegistry's
isGameFinalized() to return true before they can claim their bonds. This
implies a wait period of at least the disputeGameFinalityDelaySeconds variable from the OptimismPortal contract.
After the game is finalized, bonds can be distributed.
Bond Distribution Mode
The FDG will in most cases distribute bonds to the winners of the game after it is resolved and finalized, but in special cases will refund the bonds to the original depositor.
Normal Mode
In normal mode, the FDG will distribute bonds to the winners of the game after it is resolved and finalized.
Refund Mode
In refund mode, the FDG will refund the bonds to the original depositor.
Game Closure
The FaultDisputeGame contract can be closed after finalization via the closeGame() function.
closeGame must do the following:
- Verify the game is resolved and finalized according to the Anchor State Registry
- Attempt to set this game as the new anchor game.
- Determine the bond distribution mode based on whether the AnchorStateRegistry's
isGameProper()returnstrue. - Emit a
GameClosedevent with the chosen distribution mode.
Claiming Credit
There is a 2-step process to claim credit. First, claimCredit(address claimant) should be called to unlock the credit
from the DelayedWETH contract. After DelayedWETH's delay period has passed,
claimCredit should be called again to withdraw the credit.
The claimCredit(address claimant) function must do the following:
- Call
closeGame()to determine the distribution mode if not already closed.- In NORMAL mode: Distribute credit from the standard
normalModeCreditmapping. - In REFUND mode: Distribute credit from the
refundModeCreditmapping.
- In NORMAL mode: Distribute credit from the standard
- If the claimant has not yet unlocked their credit, unlock it by calling
DelayedWETH.unlock(claimant, credit).- Claimant must not be able to unlock this credit again.
- If the claimant has already unlocked their credit, call
DelayedWETH.withdraw(claimant, credit)(implying a delay period) to withdraw the credit, and set claimant'screditbalances to 0.
DelayedWETH
DelayedWETH is designed to hold the bonded ETH for each
Fault Dispute Game.
DelayedWETH is an extended version of the standard WETH contract that introduces a delayed unwrap mechanism that
allows an owner address to function as a backstop in the case that a Fault Dispute Game would
incorrectly distribute bonds.
DelayedWETH is modified from WETH as follows:
DelayedWETHis an upgradeable proxy contract.DelayedWETHhas anowner()address. We typically expect this to be set to theSystem Owneraddress.DelayedWETHhas adelay()function that returns a period of time that withdrawals will be delayed.DelayedWETHhas anunlock(guy,wad)function that modifies a mapping calledwithdrawalskeyed aswithdrawals[msg.sender][guy] => WithdrawalRequestwhereWithdrawalRequestisstruct Withdrawal Request { uint256 amount, uint256 timestamp }. Whenunlockis called, the timestamp forwithdrawals[msg.sender][guy]is set to the current timestamp and the amount is increased by the given amount.DelayedWETHmodifies theWETH.withdrawfunction such that an address must provide a "sub-account" to withdraw from. The function signature becomeswithdraw(guy,wad). The function retrieveswithdrawals[msg.sender][guy]and checks that the currentblock.timestampis greater than the timestamp on the withdrawal request plus thedelay()seconds and reverts if not. It also confirms that the amount being withdrawn is less than the amount in the withdrawal request. Before completing the withdrawal, it reduces the amount contained within the withdrawal request. The originalwithdraw(wad)function becomes an alias forwithdraw(msg.sender, wad).withdraw(guy,wad)will not be callable whenSuperchainConfig.paused()istrue.DelayedWETHhas ahold(guy,wad)function that allows theowner()address to, for any holder, give itself an allowance and immediatelytransferFromthat allowance amount to itself.DelayedWETHhas ahold(guy)function that allows theowner()address to, for any holder, give itself a full allowance of the holder's balance and immediatelytransferFromthat amount to itself.DelayedWETHhas arecover()function that allows theowner()address to recover any amount of ETH from the contract.
Sub-Account Model
This specification requires that withdrawal requests specify "sub-accounts" that these requests correspond to. This
takes the form of requiring that unlock and withdraw both take an address guy parameter as input. By requiring
this extra input, withdrawals are separated between accounts and it is always possible to see how much WETH a specific
end-user of the FaultDisputeGame can withdraw at any given time. It is therefore possible for the DelayedWETH
contract to account for all bug cases within the FaultDisputeGame as long as the FaultDisputeGame always passes the
correct address into withdraw.
Delay Period
We propose a delay period of 7 days for most OP Stack chains. 7 days provides sufficient time for the owner() of the
DelayedWETH contract to act even if that owner is a large multisig that requires action from many different members
over multiple timezones.
Integration
DelayedWETH is expected to be integrated into the Fault Dispute Game as follows:
- When
FaultDisputeGame.initializeis triggered,DelayedWETH.deposit{value: msg.value}()is called. - When
FaultDisputeGame.moveis triggered,DelayedWETH.deposit{value: msg.value}()is called. - When
FaultDisputeGame.resolveClaimis triggered, the game will add to the claimant's internal credit balance. - When
FaultDisputeGame.claimCreditis triggered,DelayedWETH.withdraw(recipient, credit)is called.
sequenceDiagram
participant U as User
participant FDG as FaultDisputeGame
participant DW as DelayedWETH
U->>FDG: initialize()
FDG->>DW: deposit{value: msg.value}()
Note over DW: FDG gains balance in DW
loop move by Users
U->>FDG: move()
FDG->>DW: deposit{value: msg.value}()
Note over DW: Increases FDG balance in DW
end
loop resolveClaim by Users
U->>FDG: resolveClaim()
FDG->>FDG: Add to claimant credit
end
loop Initial claimCredit call by Users
U->>FDG: claimCredit()
FDG->>DW: unlock(recipient, bond)
end
loop Subsequent claimCredit call by Users
U->>FDG: claimCredit()
FDG->>DW: withdraw(recipient, credit)
Note over DW: Checks timer/amount for recipient
DW->>FDG: Transfer claim to FDG
FDG->>U: Transfer claim to User
end