Commit e496caa5 authored by clabby's avatar clabby Committed by GitHub

feat(ctb + challenger): Initial bond scaffolding (#8816)

* Add initial bond logic

* fmt

* swap `BondKind` -> `Position`

* Change `countered` -> `counteredBy`

* Add claimant for payouts

* bindings + semver lock

:broom:

* solvency invariant test

* fmt, :broom:

* Add static payouts test

* Update op-challenger/game/fault/player.go
Co-authored-by: default avatarcoderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* resolve conflicts

* @ajsutton review

* push -> pull

* resolve conflicts

* @refcell nit

---------
Co-authored-by: default avatarcoderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
parent e7ec5b3e
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
...@@ -32,6 +32,7 @@ var ( ...@@ -32,6 +32,7 @@ var (
methodGenesisOutputRoot = "genesisOutputRoot" methodGenesisOutputRoot = "genesisOutputRoot"
methodSplitDepth = "splitDepth" methodSplitDepth = "splitDepth"
methodL2BlockNumber = "l2BlockNumber" methodL2BlockNumber = "l2BlockNumber"
methodRequiredBond = "getRequiredBond"
) )
type FaultDisputeGameContract struct { type FaultDisputeGameContract struct {
...@@ -91,6 +92,14 @@ func (c *FaultDisputeGameContract) GetSplitDepth(ctx context.Context) (types.Dep ...@@ -91,6 +92,14 @@ func (c *FaultDisputeGameContract) GetSplitDepth(ctx context.Context) (types.Dep
return types.Depth(splitDepth.GetBigInt(0).Uint64()), nil return types.Depth(splitDepth.GetBigInt(0).Uint64()), nil
} }
func (c *FaultDisputeGameContract) GetRequiredBond(ctx context.Context, position types.Position) (*big.Int, error) {
bond, err := c.multiCaller.SingleCall(ctx, batching.BlockLatest, c.contract.Call(methodRequiredBond, position.ToGIndex()))
if err != nil {
return nil, fmt.Errorf("failed to retrieve required bond: %w", err)
}
return bond.GetBigInt(0), nil
}
func (f *FaultDisputeGameContract) UpdateOracleTx(ctx context.Context, claimIdx uint64, data *types.PreimageOracleData) (txmgr.TxCandidate, error) { func (f *FaultDisputeGameContract) UpdateOracleTx(ctx context.Context, claimIdx uint64, data *types.PreimageOracleData) (txmgr.TxCandidate, error) {
if data.IsLocal { if data.IsLocal {
return f.addLocalDataTx(claimIdx, data) return f.addLocalDataTx(claimIdx, data)
...@@ -119,6 +128,7 @@ func (f *FaultDisputeGameContract) addGlobalDataTx(ctx context.Context, data *ty ...@@ -119,6 +128,7 @@ func (f *FaultDisputeGameContract) addGlobalDataTx(ctx context.Context, data *ty
} }
return oracle.AddGlobalDataTx(data) return oracle.AddGlobalDataTx(data)
} }
func (f *FaultDisputeGameContract) GetGameDuration(ctx context.Context) (uint64, error) { func (f *FaultDisputeGameContract) GetGameDuration(ctx context.Context) (uint64, error) {
result, err := f.multiCaller.SingleCall(ctx, batching.BlockLatest, f.contract.Call(methodGameDuration)) result, err := f.multiCaller.SingleCall(ctx, batching.BlockLatest, f.contract.Call(methodGameDuration))
if err != nil { if err != nil {
...@@ -260,16 +270,20 @@ func (f *FaultDisputeGameContract) resolveCall() *batching.ContractCall { ...@@ -260,16 +270,20 @@ func (f *FaultDisputeGameContract) resolveCall() *batching.ContractCall {
func (f *FaultDisputeGameContract) decodeClaim(result *batching.CallResult, contractIndex int) types.Claim { func (f *FaultDisputeGameContract) decodeClaim(result *batching.CallResult, contractIndex int) types.Claim {
parentIndex := result.GetUint32(0) parentIndex := result.GetUint32(0)
countered := result.GetBool(1) counteredBy := result.GetAddress(1)
claim := result.GetHash(2) claimant := result.GetAddress(2)
position := result.GetBigInt(3) bond := result.GetBigInt(3)
clock := result.GetBigInt(4) claim := result.GetHash(4)
position := result.GetBigInt(5)
clock := result.GetBigInt(6)
return types.Claim{ return types.Claim{
ClaimData: types.ClaimData{ ClaimData: types.ClaimData{
Value: claim, Value: claim,
Position: types.NewPositionFromGIndex(position), Position: types.NewPositionFromGIndex(position),
Bond: bond,
}, },
Countered: countered, CounteredBy: counteredBy,
Claimant: claimant,
Clock: clock.Uint64(), Clock: clock.Uint64(),
ContractIndex: contractIndex, ContractIndex: contractIndex,
ParentContractIndex: int(parentIndex), ParentContractIndex: int(parentIndex),
......
...@@ -109,19 +109,23 @@ func TestGetClaim(t *testing.T) { ...@@ -109,19 +109,23 @@ func TestGetClaim(t *testing.T) {
stubRpc, game := setupFaultDisputeGameTest(t) stubRpc, game := setupFaultDisputeGameTest(t)
idx := big.NewInt(2) idx := big.NewInt(2)
parentIndex := uint32(1) parentIndex := uint32(1)
countered := true counteredBy := common.Address{0x01}
claimant := common.Address{0x02}
bond := big.NewInt(5)
value := common.Hash{0xab} value := common.Hash{0xab}
position := big.NewInt(2) position := big.NewInt(2)
clock := big.NewInt(1234) clock := big.NewInt(1234)
stubRpc.SetResponse(fdgAddr, methodClaim, batching.BlockLatest, []interface{}{idx}, []interface{}{parentIndex, countered, value, position, clock}) stubRpc.SetResponse(fdgAddr, methodClaim, batching.BlockLatest, []interface{}{idx}, []interface{}{parentIndex, counteredBy, claimant, bond, value, position, clock})
status, err := game.GetClaim(context.Background(), idx.Uint64()) status, err := game.GetClaim(context.Background(), idx.Uint64())
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, faultTypes.Claim{ require.Equal(t, faultTypes.Claim{
ClaimData: faultTypes.ClaimData{ ClaimData: faultTypes.ClaimData{
Value: value, Value: value,
Position: faultTypes.NewPositionFromGIndex(position), Position: faultTypes.NewPositionFromGIndex(position),
Bond: bond,
}, },
Countered: true, CounteredBy: counteredBy,
Claimant: claimant,
Clock: 1234, Clock: 1234,
ContractIndex: int(idx.Uint64()), ContractIndex: int(idx.Uint64()),
ParentContractIndex: 1, ParentContractIndex: 1,
...@@ -134,8 +138,10 @@ func TestGetAllClaims(t *testing.T) { ...@@ -134,8 +138,10 @@ func TestGetAllClaims(t *testing.T) {
ClaimData: faultTypes.ClaimData{ ClaimData: faultTypes.ClaimData{
Value: common.Hash{0xaa}, Value: common.Hash{0xaa},
Position: faultTypes.NewPositionFromGIndex(big.NewInt(1)), Position: faultTypes.NewPositionFromGIndex(big.NewInt(1)),
Bond: big.NewInt(5),
}, },
Countered: true, CounteredBy: common.Address{0x01},
Claimant: common.Address{0x02},
Clock: 1234, Clock: 1234,
ContractIndex: 0, ContractIndex: 0,
ParentContractIndex: math.MaxUint32, ParentContractIndex: math.MaxUint32,
...@@ -144,8 +150,10 @@ func TestGetAllClaims(t *testing.T) { ...@@ -144,8 +150,10 @@ func TestGetAllClaims(t *testing.T) {
ClaimData: faultTypes.ClaimData{ ClaimData: faultTypes.ClaimData{
Value: common.Hash{0xab}, Value: common.Hash{0xab},
Position: faultTypes.NewPositionFromGIndex(big.NewInt(2)), Position: faultTypes.NewPositionFromGIndex(big.NewInt(2)),
Bond: big.NewInt(5),
}, },
Countered: true, CounteredBy: common.Address{0x02},
Claimant: common.Address{0x01},
Clock: 4455, Clock: 4455,
ContractIndex: 1, ContractIndex: 1,
ParentContractIndex: 0, ParentContractIndex: 0,
...@@ -154,8 +162,9 @@ func TestGetAllClaims(t *testing.T) { ...@@ -154,8 +162,9 @@ func TestGetAllClaims(t *testing.T) {
ClaimData: faultTypes.ClaimData{ ClaimData: faultTypes.ClaimData{
Value: common.Hash{0xbb}, Value: common.Hash{0xbb},
Position: faultTypes.NewPositionFromGIndex(big.NewInt(6)), Position: faultTypes.NewPositionFromGIndex(big.NewInt(6)),
Bond: big.NewInt(5),
}, },
Countered: false, Claimant: common.Address{0x02},
Clock: 7777, Clock: 7777,
ContractIndex: 2, ContractIndex: 2,
ParentContractIndex: 1, ParentContractIndex: 1,
...@@ -229,7 +238,9 @@ func expectGetClaim(stubRpc *batchingTest.AbiBasedRpc, claim faultTypes.Claim) { ...@@ -229,7 +238,9 @@ func expectGetClaim(stubRpc *batchingTest.AbiBasedRpc, claim faultTypes.Claim) {
[]interface{}{big.NewInt(int64(claim.ContractIndex))}, []interface{}{big.NewInt(int64(claim.ContractIndex))},
[]interface{}{ []interface{}{
uint32(claim.ParentContractIndex), uint32(claim.ParentContractIndex),
claim.Countered, claim.CounteredBy,
claim.Claimant,
claim.Bond,
claim.Value, claim.Value,
claim.Position.ToGIndex(), claim.Position.ToGIndex(),
big.NewInt(int64(claim.Clock)), big.NewInt(int64(claim.Clock)),
......
...@@ -3,6 +3,7 @@ package responder ...@@ -3,6 +3,7 @@ package responder
import ( import (
"context" "context"
"fmt" "fmt"
"math/big"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/types" "github.com/ethereum-optimism/optimism/op-challenger/game/fault/types"
gameTypes "github.com/ethereum-optimism/optimism/op-challenger/game/types" gameTypes "github.com/ethereum-optimism/optimism/op-challenger/game/types"
...@@ -22,6 +23,7 @@ type GameContract interface { ...@@ -22,6 +23,7 @@ type GameContract interface {
DefendTx(parentContractIndex uint64, pivot common.Hash) (txmgr.TxCandidate, error) DefendTx(parentContractIndex uint64, pivot common.Hash) (txmgr.TxCandidate, error)
StepTx(claimIdx uint64, isAttack bool, stateData []byte, proof []byte) (txmgr.TxCandidate, error) StepTx(claimIdx uint64, isAttack bool, stateData []byte, proof []byte) (txmgr.TxCandidate, error)
UpdateOracleTx(ctx context.Context, claimIdx uint64, data *types.PreimageOracleData) (txmgr.TxCandidate, error) UpdateOracleTx(ctx context.Context, claimIdx uint64, data *types.PreimageOracleData) (txmgr.TxCandidate, error)
GetRequiredBond(ctx context.Context, position types.Position) (*big.Int, error)
} }
// FaultResponder implements the [Responder] interface to send onchain transactions. // FaultResponder implements the [Responder] interface to send onchain transactions.
...@@ -87,11 +89,20 @@ func (r *FaultResponder) PerformAction(ctx context.Context, action types.Action) ...@@ -87,11 +89,20 @@ func (r *FaultResponder) PerformAction(ctx context.Context, action types.Action)
var err error var err error
switch action.Type { switch action.Type {
case types.ActionTypeMove: case types.ActionTypeMove:
var movePos types.Position
if action.IsAttack { if action.IsAttack {
movePos = action.ParentPosition.Attack()
candidate, err = r.contract.AttackTx(uint64(action.ParentIdx), action.Value) candidate, err = r.contract.AttackTx(uint64(action.ParentIdx), action.Value)
} else { } else {
movePos = action.ParentPosition.Defend()
candidate, err = r.contract.DefendTx(uint64(action.ParentIdx), action.Value) candidate, err = r.contract.DefendTx(uint64(action.ParentIdx), action.Value)
} }
bondValue, err := r.contract.GetRequiredBond(ctx, movePos)
if err != nil {
return err
}
candidate.Value = bondValue
case types.ActionTypeStep: case types.ActionTypeStep:
candidate, err = r.contract.StepTx(uint64(action.ParentIdx), action.IsAttack, action.PreState, action.ProofData) candidate, err = r.contract.StepTx(uint64(action.ParentIdx), action.IsAttack, action.PreState, action.ProofData)
} }
......
...@@ -3,6 +3,7 @@ package responder ...@@ -3,6 +3,7 @@ package responder
import ( import (
"context" "context"
"errors" "errors"
"math/big"
"testing" "testing"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/types" "github.com/ethereum-optimism/optimism/op-challenger/game/fault/types"
...@@ -289,3 +290,7 @@ func (m *mockContract) UpdateOracleTx(_ context.Context, claimIdx uint64, data * ...@@ -289,3 +290,7 @@ func (m *mockContract) UpdateOracleTx(_ context.Context, claimIdx uint64, data *
m.updateOracleArgs = data m.updateOracleArgs = data
return txmgr.TxCandidate{TxData: ([]byte)("updateOracle")}, nil return txmgr.TxCandidate{TxData: ([]byte)("updateOracle")}, nil
} }
func (m *mockContract) GetRequiredBond(_ context.Context, position types.Position) (*big.Int, error) {
return big.NewInt(5), nil
}
...@@ -6,6 +6,7 @@ import ( ...@@ -6,6 +6,7 @@ import (
"fmt" "fmt"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/types" "github.com/ethereum-optimism/optimism/op-challenger/game/fault/types"
"github.com/ethereum/go-ethereum/common"
) )
type GameSolver struct { type GameSolver struct {
...@@ -50,7 +51,7 @@ func (s *GameSolver) CalculateNextActions(ctx context.Context, game types.Game) ...@@ -50,7 +51,7 @@ func (s *GameSolver) CalculateNextActions(ctx context.Context, game types.Game)
} }
func (s *GameSolver) calculateStep(ctx context.Context, game types.Game, agreeWithRootClaim bool, claim types.Claim) (*types.Action, error) { func (s *GameSolver) calculateStep(ctx context.Context, game types.Game, agreeWithRootClaim bool, claim types.Claim) (*types.Action, error) {
if claim.Countered { if claim.CounteredBy != (common.Address{}) {
return nil, nil return nil, nil
} }
if game.AgreeWithClaimLevel(claim, agreeWithRootClaim) { if game.AgreeWithClaimLevel(claim, agreeWithRootClaim) {
...@@ -64,12 +65,13 @@ func (s *GameSolver) calculateStep(ctx context.Context, game types.Game, agreeWi ...@@ -64,12 +65,13 @@ func (s *GameSolver) calculateStep(ctx context.Context, game types.Game, agreeWi
return nil, err return nil, err
} }
return &types.Action{ return &types.Action{
Type: types.ActionTypeStep, Type: types.ActionTypeStep,
ParentIdx: step.LeafClaim.ContractIndex, ParentIdx: step.LeafClaim.ContractIndex,
IsAttack: step.IsAttack, ParentPosition: step.LeafClaim.Position,
PreState: step.PreState, IsAttack: step.IsAttack,
ProofData: step.ProofData, PreState: step.PreState,
OracleData: step.OracleData, ProofData: step.ProofData,
OracleData: step.OracleData,
}, nil }, nil
} }
...@@ -85,9 +87,10 @@ func (s *GameSolver) calculateMove(ctx context.Context, game types.Game, agreeWi ...@@ -85,9 +87,10 @@ func (s *GameSolver) calculateMove(ctx context.Context, game types.Game, agreeWi
return nil, nil return nil, nil
} }
return &types.Action{ return &types.Action{
Type: types.ActionTypeMove, Type: types.ActionTypeMove,
IsAttack: !game.DefendsParent(*move), IsAttack: !game.DefendsParent(*move),
ParentIdx: move.ParentContractIndex, ParentIdx: move.ParentContractIndex,
Value: move.Value, ParentPosition: claim.Position,
Value: move.Value,
}, nil }, nil
} }
...@@ -93,8 +93,8 @@ func TestCalculateNextActions(t *testing.T) { ...@@ -93,8 +93,8 @@ func TestCalculateNextActions(t *testing.T) {
test.setupGame(builder) test.setupGame(builder)
game := builder.Game game := builder.Game
for i, claim := range game.Claims() { for i, claim := range game.Claims() {
t.Logf("Claim %v: Pos: %v TraceIdx: %v ParentIdx: %v, Countered: %v, Value: %v", t.Logf("Claim %v: Pos: %v TraceIdx: %v ParentIdx: %v, CounteredBy: %v, Value: %v",
i, claim.Position.ToGIndex(), claim.Position.TraceIndex(maxDepth), claim.ParentContractIndex, claim.Countered, claim.Value) i, claim.Position.ToGIndex(), claim.Position.TraceIndex(maxDepth), claim.ParentContractIndex, claim.CounteredBy, claim.Value)
} }
solver := NewGameSolver(maxDepth, trace.NewSimpleTraceAccessor(claimBuilder.CorrectTraceProvider())) solver := NewGameSolver(maxDepth, trace.NewSimpleTraceAccessor(claimBuilder.CorrectTraceProvider()))
......
...@@ -90,10 +90,11 @@ func (s *GameBuilderSeq) ExpectAttack() *GameBuilderSeq { ...@@ -90,10 +90,11 @@ func (s *GameBuilderSeq) ExpectAttack() *GameBuilderSeq {
newPos := s.lastClaim.Position.Attack() newPos := s.lastClaim.Position.Attack()
value := s.builder.CorrectClaimAtPosition(newPos) value := s.builder.CorrectClaimAtPosition(newPos)
s.gameBuilder.ExpectedActions = append(s.gameBuilder.ExpectedActions, types.Action{ s.gameBuilder.ExpectedActions = append(s.gameBuilder.ExpectedActions, types.Action{
Type: types.ActionTypeMove, Type: types.ActionTypeMove,
ParentIdx: s.lastClaim.ContractIndex, ParentIdx: s.lastClaim.ContractIndex,
IsAttack: true, ParentPosition: s.lastClaim.Position,
Value: value, IsAttack: true,
Value: value,
}) })
return s return s
} }
...@@ -102,10 +103,11 @@ func (s *GameBuilderSeq) ExpectDefend() *GameBuilderSeq { ...@@ -102,10 +103,11 @@ func (s *GameBuilderSeq) ExpectDefend() *GameBuilderSeq {
newPos := s.lastClaim.Position.Defend() newPos := s.lastClaim.Position.Defend()
value := s.builder.CorrectClaimAtPosition(newPos) value := s.builder.CorrectClaimAtPosition(newPos)
s.gameBuilder.ExpectedActions = append(s.gameBuilder.ExpectedActions, types.Action{ s.gameBuilder.ExpectedActions = append(s.gameBuilder.ExpectedActions, types.Action{
Type: types.ActionTypeMove, Type: types.ActionTypeMove,
ParentIdx: s.lastClaim.ContractIndex, ParentIdx: s.lastClaim.ContractIndex,
IsAttack: false, ParentPosition: s.lastClaim.Position,
Value: value, IsAttack: false,
Value: value,
}) })
return s return s
} }
...@@ -113,12 +115,13 @@ func (s *GameBuilderSeq) ExpectDefend() *GameBuilderSeq { ...@@ -113,12 +115,13 @@ func (s *GameBuilderSeq) ExpectDefend() *GameBuilderSeq {
func (s *GameBuilderSeq) ExpectStepAttack() *GameBuilderSeq { func (s *GameBuilderSeq) ExpectStepAttack() *GameBuilderSeq {
traceIdx := s.lastClaim.TraceIndex(s.builder.maxDepth) traceIdx := s.lastClaim.TraceIndex(s.builder.maxDepth)
s.gameBuilder.ExpectedActions = append(s.gameBuilder.ExpectedActions, types.Action{ s.gameBuilder.ExpectedActions = append(s.gameBuilder.ExpectedActions, types.Action{
Type: types.ActionTypeStep, Type: types.ActionTypeStep,
ParentIdx: s.lastClaim.ContractIndex, ParentIdx: s.lastClaim.ContractIndex,
IsAttack: true, ParentPosition: s.lastClaim.Position,
PreState: s.builder.CorrectPreState(traceIdx), IsAttack: true,
ProofData: s.builder.CorrectProofData(traceIdx), PreState: s.builder.CorrectPreState(traceIdx),
OracleData: s.builder.CorrectOracleData(traceIdx), ProofData: s.builder.CorrectProofData(traceIdx),
OracleData: s.builder.CorrectOracleData(traceIdx),
}) })
return s return s
} }
...@@ -126,12 +129,13 @@ func (s *GameBuilderSeq) ExpectStepAttack() *GameBuilderSeq { ...@@ -126,12 +129,13 @@ func (s *GameBuilderSeq) ExpectStepAttack() *GameBuilderSeq {
func (s *GameBuilderSeq) ExpectStepDefend() *GameBuilderSeq { func (s *GameBuilderSeq) ExpectStepDefend() *GameBuilderSeq {
traceIdx := new(big.Int).Add(s.lastClaim.TraceIndex(s.builder.maxDepth), big.NewInt(1)) traceIdx := new(big.Int).Add(s.lastClaim.TraceIndex(s.builder.maxDepth), big.NewInt(1))
s.gameBuilder.ExpectedActions = append(s.gameBuilder.ExpectedActions, types.Action{ s.gameBuilder.ExpectedActions = append(s.gameBuilder.ExpectedActions, types.Action{
Type: types.ActionTypeStep, Type: types.ActionTypeStep,
ParentIdx: s.lastClaim.ContractIndex, ParentIdx: s.lastClaim.ContractIndex,
IsAttack: false, ParentPosition: s.lastClaim.Position,
PreState: s.builder.CorrectPreState(traceIdx), IsAttack: false,
ProofData: s.builder.CorrectProofData(traceIdx), PreState: s.builder.CorrectPreState(traceIdx),
OracleData: s.builder.CorrectOracleData(traceIdx), ProofData: s.builder.CorrectProofData(traceIdx),
OracleData: s.builder.CorrectOracleData(traceIdx),
}) })
return s return s
} }
...@@ -14,9 +14,10 @@ const ( ...@@ -14,9 +14,10 @@ const (
) )
type Action struct { type Action struct {
Type ActionType Type ActionType
ParentIdx int ParentIdx int
IsAttack bool ParentPosition Position
IsAttack bool
// Moves // Moves
Value common.Hash Value common.Hash
......
...@@ -89,6 +89,7 @@ type TraceProvider interface { ...@@ -89,6 +89,7 @@ type TraceProvider interface {
// ClaimData is the core of a claim. It must be unique inside a specific game. // ClaimData is the core of a claim. It must be unique inside a specific game.
type ClaimData struct { type ClaimData struct {
Value common.Hash Value common.Hash
Bond *big.Int
Position Position
} }
...@@ -105,12 +106,13 @@ func (c *ClaimData) ValueBytes() [32]byte { ...@@ -105,12 +106,13 @@ func (c *ClaimData) ValueBytes() [32]byte {
// and the Parent field is empty & meaningless. // and the Parent field is empty & meaningless.
type Claim struct { type Claim struct {
ClaimData ClaimData
// WARN: Countered is a mutable field in the FaultDisputeGame contract // WARN: CounteredBy is a mutable field in the FaultDisputeGame contract
// and rely on it for determining whether to step on leaf claims. // and rely on it for determining whether to step on leaf claims.
// When caching is implemented for the Challenger, this will need // When caching is implemented for the Challenger, this will need
// to be changed/removed to avoid invalid/stale contract state. // to be changed/removed to avoid invalid/stale contract state.
Countered bool CounteredBy common.Address
Clock uint64 Claimant common.Address
Clock uint64
// Location of the claim & it's parent inside the contract. Does not exist // Location of the claim & it's parent inside the contract. Does not exist
// for claims that have not made it to the contract. // for claims that have not made it to the contract.
ContractIndex int ContractIndex int
......
...@@ -79,7 +79,7 @@ func (c *ClaimHelper) WaitForCountered(ctx context.Context) { ...@@ -79,7 +79,7 @@ func (c *ClaimHelper) WaitForCountered(ctx context.Context) {
defer cancel() defer cancel()
err := wait.For(timedCtx, time.Second, func() (bool, error) { err := wait.For(timedCtx, time.Second, func() (bool, error) {
latestData := c.game.getClaim(ctx, c.index) latestData := c.game.getClaim(ctx, c.index)
return latestData.Countered, nil return latestData.CounteredBy != common.Address{}, nil
}) })
if err != nil { // Avoid waiting time capturing game data when there's no error if err != nil { // Avoid waiting time capturing game data when there's no error
c.require.NoErrorf(err, "Claim %v was not countered\n%v", c.index, c.game.gameData(ctx)) c.require.NoErrorf(err, "Claim %v was not countered\n%v", c.index, c.game.gameData(ctx))
......
...@@ -158,7 +158,7 @@ func (g *FaultGameHelper) WaitForClaimAtMaxDepth(ctx context.Context, countered ...@@ -158,7 +158,7 @@ func (g *FaultGameHelper) WaitForClaimAtMaxDepth(ctx context.Context, countered
fmt.Sprintf("Could not find claim depth %v with countered=%v", maxDepth, countered), fmt.Sprintf("Could not find claim depth %v with countered=%v", maxDepth, countered),
func(claim ContractClaim) bool { func(claim ContractClaim) bool {
pos := types.NewPositionFromGIndex(claim.Position) pos := types.NewPositionFromGIndex(claim.Position)
return pos.Depth() == maxDepth && claim.Countered == countered return pos.Depth() == maxDepth && (claim.CounteredBy != common.Address{}) == countered
}) })
} }
...@@ -167,7 +167,7 @@ func (g *FaultGameHelper) WaitForAllClaimsCountered(ctx context.Context) { ...@@ -167,7 +167,7 @@ func (g *FaultGameHelper) WaitForAllClaimsCountered(ctx context.Context) {
ctx, ctx,
"Did not find all claims countered", "Did not find all claims countered",
func(claim ContractClaim) bool { func(claim ContractClaim) bool {
return !claim.Countered return claim.CounteredBy == common.Address{}
}) })
} }
...@@ -352,7 +352,7 @@ func (g *FaultGameHelper) gameData(ctx context.Context) string { ...@@ -352,7 +352,7 @@ func (g *FaultGameHelper) gameData(ctx context.Context) string {
pos := types.NewPositionFromGIndex(claim.Position) pos := types.NewPositionFromGIndex(claim.Position)
info = info + fmt.Sprintf("%v - Position: %v, Depth: %v, IndexAtDepth: %v Trace Index: %v, Value: %v, Countered: %v, ParentIndex: %v\n", info = info + fmt.Sprintf("%v - Position: %v, Depth: %v, IndexAtDepth: %v Trace Index: %v, Value: %v, Countered: %v, ParentIndex: %v\n",
i, claim.Position.Int64(), pos.Depth(), pos.IndexAtDepth(), pos.TraceIndex(maxDepth), common.Hash(claim.Claim).Hex(), claim.Countered, claim.ParentIndex) i, claim.Position.Int64(), pos.Depth(), pos.IndexAtDepth(), pos.TraceIndex(maxDepth), common.Hash(claim.Claim).Hex(), claim.CounteredBy, claim.ParentIndex)
} }
status, err := g.game.Status(opts) status, err := g.game.Status(opts)
g.require.NoError(err, "Load game status") g.require.NoError(err, "Load game status")
......
...@@ -146,7 +146,9 @@ func (g *OutputGameHelper) WaitForClaimCount(ctx context.Context, count int64) { ...@@ -146,7 +146,9 @@ func (g *OutputGameHelper) WaitForClaimCount(ctx context.Context, count int64) {
type ContractClaim struct { type ContractClaim struct {
ParentIndex uint32 ParentIndex uint32
Countered bool CounteredBy common.Address
Claimant common.Address
Bond *big.Int
Claim [32]byte Claim [32]byte
Position *big.Int Position *big.Int
Clock *big.Int Clock *big.Int
...@@ -255,7 +257,7 @@ func (g *OutputGameHelper) WaitForClaimAtMaxDepth(ctx context.Context, countered ...@@ -255,7 +257,7 @@ func (g *OutputGameHelper) WaitForClaimAtMaxDepth(ctx context.Context, countered
fmt.Sprintf("Could not find claim depth %v with countered=%v", maxDepth, countered), fmt.Sprintf("Could not find claim depth %v with countered=%v", maxDepth, countered),
func(_ int64, claim ContractClaim) bool { func(_ int64, claim ContractClaim) bool {
pos := types.NewPositionFromGIndex(claim.Position) pos := types.NewPositionFromGIndex(claim.Position)
return pos.Depth() == maxDepth && claim.Countered == countered return pos.Depth() == maxDepth && (claim.CounteredBy != common.Address{}) == countered
}) })
} }
...@@ -264,7 +266,7 @@ func (g *OutputGameHelper) WaitForAllClaimsCountered(ctx context.Context) { ...@@ -264,7 +266,7 @@ func (g *OutputGameHelper) WaitForAllClaimsCountered(ctx context.Context) {
ctx, ctx,
"Did not find all claims countered", "Did not find all claims countered",
func(claim ContractClaim) bool { func(claim ContractClaim) bool {
return !claim.Countered return claim.CounteredBy == common.Address{}
}) })
} }
...@@ -478,7 +480,7 @@ func (g *OutputGameHelper) gameData(ctx context.Context) string { ...@@ -478,7 +480,7 @@ func (g *OutputGameHelper) gameData(ctx context.Context) string {
} }
} }
info = info + fmt.Sprintf("%v - Position: %v, Depth: %v, IndexAtDepth: %v Trace Index: %v, Value: %v, Countered: %v, ParentIndex: %v %v\n", info = info + fmt.Sprintf("%v - Position: %v, Depth: %v, IndexAtDepth: %v Trace Index: %v, Value: %v, Countered: %v, ParentIndex: %v %v\n",
i, claim.Position.Int64(), pos.Depth(), pos.IndexAtDepth(), pos.TraceIndex(maxDepth), common.Hash(claim.Claim).Hex(), claim.Countered, claim.ParentIndex, extra) i, claim.Position.Int64(), pos.Depth(), pos.IndexAtDepth(), pos.TraceIndex(maxDepth), common.Hash(claim.Claim).Hex(), claim.CounteredBy, claim.ParentIndex, extra)
} }
l2BlockNum := g.L2BlockNum(ctx) l2BlockNum := g.L2BlockNum(ctx)
status, err := g.game.Status(opts) status, err := g.game.Status(opts)
......
# `FaultDisputeGame` Invariants
## FaultDisputeGame always returns all ETH on total resolution
**Test:** [`FaultDisputeGame.t.sol#L38`](../test/invariants/FaultDisputeGame.t.sol#L38)
The FaultDisputeGame contract should always return all ETH in the contract to the correct recipients upon resolution of all outstanding claims. There may never be any ETH left in the contract after a full resolution.
\ No newline at end of file
...@@ -11,6 +11,7 @@ This directory contains documentation for all defined invariant tests within `co ...@@ -11,6 +11,7 @@ This directory contains documentation for all defined invariant tests within `co
- [Burn.Gas](./Burn.Gas.md) - [Burn.Gas](./Burn.Gas.md)
- [CrossDomainMessenger](./CrossDomainMessenger.md) - [CrossDomainMessenger](./CrossDomainMessenger.md)
- [Encoding](./Encoding.md) - [Encoding](./Encoding.md)
- [FaultDisputeGame](./FaultDisputeGame.md)
- [Hashing](./Hashing.md) - [Hashing](./Hashing.md)
- [L2OutputOracle](./L2OutputOracle.md) - [L2OutputOracle](./L2OutputOracle.md)
- [OptimismPortal](./OptimismPortal.md) - [OptimismPortal](./OptimismPortal.md)
......
...@@ -61,11 +61,21 @@ contract FaultDisputeGameViz is Script, FaultDisputeGame_Init { ...@@ -61,11 +61,21 @@ contract FaultDisputeGameViz is Script, FaultDisputeGame_Init {
uint256 numClaims = uint256(vm.load(address(gameProxy), bytes32(uint256(1)))); uint256 numClaims = uint256(vm.load(address(gameProxy), bytes32(uint256(1))));
IFaultDisputeGame.ClaimData[] memory gameData = new IFaultDisputeGame.ClaimData[](numClaims); IFaultDisputeGame.ClaimData[] memory gameData = new IFaultDisputeGame.ClaimData[](numClaims);
for (uint256 i = 0; i < numClaims; i++) { for (uint256 i = 0; i < numClaims; i++) {
(uint32 parentIndex, bool countered, Claim claim, Position position, Clock clock) = gameProxy.claimData(i); (
uint32 parentIndex,
address countered,
address claimant,
uint128 bond,
Claim claim,
Position position,
Clock clock
) = gameProxy.claimData(i);
gameData[i] = IFaultDisputeGame.ClaimData({ gameData[i] = IFaultDisputeGame.ClaimData({
parentIndex: parentIndex, parentIndex: parentIndex,
countered: countered, counteredBy: countered,
claimant: claimant,
bond: bond,
claim: claim, claim: claim,
position: position, position: position,
clock: clock clock: clock
......
...@@ -88,12 +88,12 @@ ...@@ -88,12 +88,12 @@
"sourceCodeHash": "0x1afb1d392e8f6a58ff86ea7f648e0d1756d4ba8d0d964279d58a390deaa53b7e" "sourceCodeHash": "0x1afb1d392e8f6a58ff86ea7f648e0d1756d4ba8d0d964279d58a390deaa53b7e"
}, },
"src/dispute/DisputeGameFactory.sol": { "src/dispute/DisputeGameFactory.sol": {
"initCodeHash": "0x6dc67d5bb1496b1360aeb7447633f28689327ee1f2df632f20d257fe8fa75c70", "initCodeHash": "0x79ede9274590494d776c9e2ecdf006cb7106f97490841ea85b52fc8eb18cd7b9",
"sourceCodeHash": "0xb6cc80e4e2c5bb6b527d4b523eafb72facff75cb25de036052eab380cb2d3213" "sourceCodeHash": "0x6e3a697764840cd30ca4b3e514528b832ce80bb412ac958b5ec6b751a07d735f"
}, },
"src/dispute/FaultDisputeGame.sol": { "src/dispute/FaultDisputeGame.sol": {
"initCodeHash": "0xc4a78ec3e597821009e07f833edc8718b5c2211b09f9a74f43658b7576e746e0", "initCodeHash": "0xe3c50aa8ab4ba7a4b4b6d5752a3a3acc2ca0efccbc086df8e00f85d4d696ab15",
"sourceCodeHash": "0x684aa7cc10c4c88753829f886a5e9aacff3281a2aeb485438cc7b9318c2768e7" "sourceCodeHash": "0xbc5e4e6ec9d4438494cacd6ed284456b41a9b6e090fa4b39a59cf122c8c3667d"
}, },
"src/legacy/DeployerWhitelist.sol": { "src/legacy/DeployerWhitelist.sol": {
"initCodeHash": "0x8de80fb23b26dd9d849f6328e56ea7c173cd9e9ce1f05c9beea559d1720deb3d", "initCodeHash": "0x8de80fb23b26dd9d849f6328e56ea7c173cd9e9ce1f05c9beea559d1720deb3d",
......
...@@ -30,7 +30,7 @@ ...@@ -30,7 +30,7 @@
"type": "address" "type": "address"
} }
], ],
"stateMutability": "nonpayable", "stateMutability": "payable",
"type": "function" "type": "function"
}, },
{ {
......
...@@ -99,6 +99,19 @@ ...@@ -99,6 +99,19 @@
"stateMutability": "payable", "stateMutability": "payable",
"type": "function" "type": "function"
}, },
{
"inputs": [
{
"internalType": "address",
"name": "_recipient",
"type": "address"
}
],
"name": "claimCredit",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{ {
"inputs": [ "inputs": [
{ {
...@@ -115,9 +128,19 @@ ...@@ -115,9 +128,19 @@
"type": "uint32" "type": "uint32"
}, },
{ {
"internalType": "bool", "internalType": "address",
"name": "countered", "name": "counteredBy",
"type": "bool" "type": "address"
},
{
"internalType": "address",
"name": "claimant",
"type": "address"
},
{
"internalType": "uint128",
"name": "bond",
"type": "uint128"
}, },
{ {
"internalType": "Claim", "internalType": "Claim",
...@@ -164,6 +187,25 @@ ...@@ -164,6 +187,25 @@
"stateMutability": "view", "stateMutability": "view",
"type": "function" "type": "function"
}, },
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "credit",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{ {
"inputs": [ "inputs": [
{ {
...@@ -270,11 +312,30 @@ ...@@ -270,11 +312,30 @@
"stateMutability": "view", "stateMutability": "view",
"type": "function" "type": "function"
}, },
{
"inputs": [
{
"internalType": "Position",
"name": "_position",
"type": "uint128"
}
],
"name": "getRequiredBond",
"outputs": [
{
"internalType": "uint256",
"name": "requiredBond_",
"type": "uint256"
}
],
"stateMutability": "pure",
"type": "function"
},
{ {
"inputs": [], "inputs": [],
"name": "initialize", "name": "initialize",
"outputs": [], "outputs": [],
"stateMutability": "nonpayable", "stateMutability": "payable",
"type": "function" "type": "function"
}, },
{ {
...@@ -514,6 +575,11 @@ ...@@ -514,6 +575,11 @@
"name": "AlreadyInitialized", "name": "AlreadyInitialized",
"type": "error" "type": "error"
}, },
{
"inputs": [],
"name": "BondTransferFailed",
"type": "error"
},
{ {
"inputs": [], "inputs": [],
"name": "CannotDefendRootClaim", "name": "CannotDefendRootClaim",
...@@ -554,6 +620,11 @@ ...@@ -554,6 +620,11 @@
"name": "GameNotInProgress", "name": "GameNotInProgress",
"type": "error" "type": "error"
}, },
{
"inputs": [],
"name": "InsufficientBond",
"type": "error"
},
{ {
"inputs": [], "inputs": [],
"name": "InvalidLocalIdent", "name": "InvalidLocalIdent",
......
...@@ -36,30 +36,37 @@ ...@@ -36,30 +36,37 @@
}, },
{ {
"bytes": "32", "bytes": "32",
"label": "claims", "label": "credit",
"offset": 0, "offset": 0,
"slot": "3", "slot": "3",
"type": "mapping(address => uint256)"
},
{
"bytes": "32",
"label": "claims",
"offset": 0,
"slot": "4",
"type": "mapping(ClaimHash => bool)" "type": "mapping(ClaimHash => bool)"
}, },
{ {
"bytes": "32", "bytes": "32",
"label": "subgames", "label": "subgames",
"offset": 0, "offset": 0,
"slot": "4", "slot": "5",
"type": "mapping(uint256 => uint256[])" "type": "mapping(uint256 => uint256[])"
}, },
{ {
"bytes": "1", "bytes": "1",
"label": "subgameAtRootResolved", "label": "subgameAtRootResolved",
"offset": 0, "offset": 0,
"slot": "5", "slot": "6",
"type": "bool" "type": "bool"
}, },
{ {
"bytes": "1", "bytes": "1",
"label": "initialized", "label": "initialized",
"offset": 1, "offset": 1,
"slot": "5", "slot": "6",
"type": "bool" "type": "bool"
} }
] ]
\ No newline at end of file
...@@ -87,6 +87,7 @@ contract DisputeGameFactory is OwnableUpgradeable, IDisputeGameFactory, ISemver ...@@ -87,6 +87,7 @@ contract DisputeGameFactory is OwnableUpgradeable, IDisputeGameFactory, ISemver
bytes calldata _extraData bytes calldata _extraData
) )
external external
payable
returns (IDisputeGame proxy_) returns (IDisputeGame proxy_)
{ {
// Grab the implementation contract for the given `GameType`. // Grab the implementation contract for the given `GameType`.
...@@ -97,7 +98,7 @@ contract DisputeGameFactory is OwnableUpgradeable, IDisputeGameFactory, ISemver ...@@ -97,7 +98,7 @@ contract DisputeGameFactory is OwnableUpgradeable, IDisputeGameFactory, ISemver
// Clone the implementation contract and initialize it with the given parameters. // Clone the implementation contract and initialize it with the given parameters.
proxy_ = IDisputeGame(address(impl).clone(abi.encodePacked(_rootClaim, _extraData))); proxy_ = IDisputeGame(address(impl).clone(abi.encodePacked(_rootClaim, _extraData)));
proxy_.initialize(); proxy_.initialize{ value: msg.value }();
// Compute the unique identifier for the dispute game. // Compute the unique identifier for the dispute game.
Hash uuid = getGameUUID(_gameType, _rootClaim, _extraData); Hash uuid = getGameUUID(_gameType, _rootClaim, _extraData);
......
...@@ -65,6 +65,9 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, ISemver { ...@@ -65,6 +65,9 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, ISemver {
/// @notice An append-only array of all claims made during the dispute game. /// @notice An append-only array of all claims made during the dispute game.
ClaimData[] public claimData; ClaimData[] public claimData;
/// @notice Credited balances for winning participants.
mapping(address => uint256) public credit;
/// @notice An internal mapping to allow for constant-time lookups of existing claims. /// @notice An internal mapping to allow for constant-time lookups of existing claims.
mapping(ClaimHash => bool) internal claims; mapping(ClaimHash => bool) internal claims;
...@@ -78,8 +81,8 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, ISemver { ...@@ -78,8 +81,8 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, ISemver {
bool internal initialized; bool internal initialized;
/// @notice Semantic version. /// @notice Semantic version.
/// @custom:semver 0.0.22 /// @custom:semver 0.0.23
string public constant version = "0.0.22"; string public constant version = "0.0.23";
/// @param _gameType The type ID of the game. /// @param _gameType The type ID of the game.
/// @param _absolutePrestate The absolute prestate of the instruction trace. /// @param _absolutePrestate The absolute prestate of the instruction trace.
...@@ -184,7 +187,7 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, ISemver { ...@@ -184,7 +187,7 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, ISemver {
// Set the parent claim as countered. We do not need to append a new claim to the game; // Set the parent claim as countered. We do not need to append a new claim to the game;
// instead, we can just set the existing parent as countered. // instead, we can just set the existing parent as countered.
parent.countered = true; parent.counteredBy = msg.sender;
} }
/// @notice Generic move function, used for both `attack` and `defend` moves. /// @notice Generic move function, used for both `attack` and `defend` moves.
...@@ -224,6 +227,9 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, ISemver { ...@@ -224,6 +227,9 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, ISemver {
_verifyExecBisectionRoot(_claim, _challengeIndex, parentPos, _isAttack); _verifyExecBisectionRoot(_claim, _challengeIndex, parentPos, _isAttack);
} }
// INVARIANT: The `msg.value` must be sufficient to cover the required bond.
if (getRequiredBond(nextPosition) > msg.value) revert InsufficientBond();
// Fetch the grandparent clock, if it exists. // Fetch the grandparent clock, if it exists.
// The grandparent clock should always exist unless the parent is the root claim. // The grandparent clock should always exist unless the parent is the root claim.
Clock grandparentClock; Clock grandparentClock;
...@@ -262,15 +268,17 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, ISemver { ...@@ -262,15 +268,17 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, ISemver {
claimData.push( claimData.push(
ClaimData({ ClaimData({
parentIndex: uint32(_challengeIndex), parentIndex: uint32(_challengeIndex),
counteredBy: address(0),
claimant: msg.sender,
bond: uint128(msg.value),
claim: _claim, claim: _claim,
position: nextPosition, position: nextPosition,
clock: nextClock, clock: nextClock
countered: false
}) })
); );
// Set the parent claim as countered. // Set the parent claim as countered.
claimData[_challengeIndex].countered = true; claimData[_challengeIndex].counteredBy = msg.sender;
// Update the subgame rooted at the parent claim. // Update the subgame rooted at the parent claim.
subgames[_challengeIndex].push(claimData.length - 1); subgames[_challengeIndex].push(claimData.length - 1);
...@@ -350,7 +358,7 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, ISemver { ...@@ -350,7 +358,7 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, ISemver {
if (!subgameAtRootResolved) revert OutOfOrderResolution(); if (!subgameAtRootResolved) revert OutOfOrderResolution();
// Update the global game status; The dispute has concluded. // Update the global game status; The dispute has concluded.
status_ = claimData[0].countered ? GameStatus.CHALLENGER_WINS : GameStatus.DEFENDER_WINS; status_ = claimData[0].counteredBy == address(0) ? GameStatus.DEFENDER_WINS : GameStatus.CHALLENGER_WINS;
resolvedAt = Timestamp.wrap(uint64(block.timestamp)); resolvedAt = Timestamp.wrap(uint64(block.timestamp));
emit Resolved(status = status_); emit Resolved(status = status_);
...@@ -374,14 +382,19 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, ISemver { ...@@ -374,14 +382,19 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, ISemver {
uint256 challengeIndicesLen = challengeIndices.length; uint256 challengeIndicesLen = challengeIndices.length;
// INVARIANT: Cannot resolve subgames twice // INVARIANT: Cannot resolve subgames twice
// Uncontested claims are resolved implicitly unless they are the root claim if (_claimIndex == 0 && subgameAtRootResolved) {
if ((_claimIndex == 0 && subgameAtRootResolved) || (challengeIndicesLen == 0 && _claimIndex != 0)) {
revert ClaimAlreadyResolved(); revert ClaimAlreadyResolved();
} }
// Assume parent is honest until proven otherwise // Uncontested claims are resolved implicitly unless they are the root claim. Pay out the bond to the claimant
bool countered = false; // and return early.
if (challengeIndicesLen == 0 && _claimIndex != 0) {
_distributeBond(parent.claimant, parent);
return;
}
// Assume parent is honest until proven otherwise
address countered = address(0);
for (uint256 i = 0; i < challengeIndicesLen; ++i) { for (uint256 i = 0; i < challengeIndicesLen; ++i) {
uint256 challengeIndex = challengeIndices[i]; uint256 challengeIndex = challengeIndices[i];
...@@ -391,15 +404,19 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, ISemver { ...@@ -391,15 +404,19 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, ISemver {
ClaimData storage claim = claimData[challengeIndex]; ClaimData storage claim = claimData[challengeIndex];
// Ignore false claims // Ignore false claims
if (!claim.countered) { if (claim.counteredBy == address(0)) {
countered = true; countered = msg.sender;
break; break;
} }
} }
// If the parent was not successfully countered, pay out the parent's bond to the claimant.
// If the parent was successfully countered, pay out the parent's bond to the challenger.
_distributeBond(countered == address(0) ? parent.claimant : countered, parent);
// Once a subgame is resolved, we percolate the result up the DAG so subsequent calls to // Once a subgame is resolved, we percolate the result up the DAG so subsequent calls to
// resolveClaim will not need to traverse this subgame. // resolveClaim will not need to traverse this subgame.
parent.countered = countered; parent.counteredBy = countered;
// Resolved subgames have no entries // Resolved subgames have no entries
delete subgames[_claimIndex]; delete subgames[_claimIndex];
...@@ -434,7 +451,7 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, ISemver { ...@@ -434,7 +451,7 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, ISemver {
//////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////
/// @inheritdoc IInitializable /// @inheritdoc IInitializable
function initialize() external { function initialize() external payable {
// SAFETY: Any revert in this function will bubble up to the DisputeGameFactory and // SAFETY: Any revert in this function will bubble up to the DisputeGameFactory and
// prevent the game from being created. // prevent the game from being created.
// //
...@@ -465,14 +482,19 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, ISemver { ...@@ -465,14 +482,19 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, ISemver {
} }
} }
// INVARIANT: The `msg.value` must be sufficient to cover the required bond.
if (getRequiredBond(ROOT_POSITION) > msg.value) revert InsufficientBond();
// Set the root claim // Set the root claim
claimData.push( claimData.push(
ClaimData({ ClaimData({
parentIndex: type(uint32).max, parentIndex: type(uint32).max,
counteredBy: address(0),
claimant: tx.origin,
bond: uint128(msg.value),
claim: rootClaim(), claim: rootClaim(),
position: ROOT_POSITION, position: ROOT_POSITION,
clock: LibClock.wrap(Duration.wrap(0), Timestamp.wrap(uint64(block.timestamp))), clock: LibClock.wrap(Duration.wrap(0), Timestamp.wrap(uint64(block.timestamp)))
countered: false
}) })
); );
...@@ -491,6 +513,27 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, ISemver { ...@@ -491,6 +513,27 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, ISemver {
len_ = claimData.length; len_ = claimData.length;
} }
/// @notice Returns the required bond for a given move kind.
/// @param _position The position of the bonded interaction.
/// @return requiredBond_ The required ETH bond for the given move, in wei.
function getRequiredBond(Position _position) public pure returns (uint256 requiredBond_) {
// TODO
_position;
requiredBond_ = 0;
}
/// @notice Claim the credit belonging to the recipient address.
/// @param _recipient The owner and recipient of the credit.
function claimCredit(address _recipient) external {
// Remove the credit from the recipient prior to performing the external call.
uint256 recipientCredit = credit[_recipient];
credit[_recipient] = 0;
// Transfer the credit to the recipient.
(bool success,) = _recipient.call{ value: recipientCredit }(hex"");
if (!success) revert BondTransferFailed();
}
//////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////
// IMMUTABLE GETTERS // // IMMUTABLE GETTERS //
//////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////
...@@ -534,6 +577,19 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, ISemver { ...@@ -534,6 +577,19 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, ISemver {
// HELPERS // // HELPERS //
//////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////
/// @notice Pays out the bond of a claim to a given recipient.
/// @param _recipient The recipient of the bond.
/// @param _bonded The claim to pay out the bond of.
function _distributeBond(address _recipient, ClaimData storage _bonded) internal {
// Set all bits in the bond value to indicate that the bond has been paid out.
uint256 bond = _bonded.bond;
if (bond == type(uint128).max) revert ClaimAlreadyResolved();
_bonded.bond = type(uint128).max;
// Increase the recipient's credit.
credit[_recipient] += bond;
}
/// @notice Verifies the integrity of an execution bisection subgame's root claim. Reverts if the claim /// @notice Verifies the integrity of an execution bisection subgame's root claim. Reverts if the claim
/// is invalid. /// is invalid.
/// @param _rootClaim The root claim of the execution bisection subgame. /// @param _rootClaim The root claim of the execution bisection subgame.
......
...@@ -71,6 +71,7 @@ interface IDisputeGameFactory { ...@@ -71,6 +71,7 @@ interface IDisputeGameFactory {
bytes calldata _extraData bytes calldata _extraData
) )
external external
payable
returns (IDisputeGame proxy_); returns (IDisputeGame proxy_);
/// @notice Sets the implementation contract for a specific `GameType`. /// @notice Sets the implementation contract for a specific `GameType`.
......
...@@ -9,10 +9,11 @@ import "src/libraries/DisputeTypes.sol"; ...@@ -9,10 +9,11 @@ import "src/libraries/DisputeTypes.sol";
/// @notice The interface for a fault proof backed dispute game. /// @notice The interface for a fault proof backed dispute game.
interface IFaultDisputeGame is IDisputeGame { interface IFaultDisputeGame is IDisputeGame {
/// @notice The `ClaimData` struct represents the data associated with a Claim. /// @notice The `ClaimData` struct represents the data associated with a Claim.
/// @dev TODO(clabby): Add bond ID information.
struct ClaimData { struct ClaimData {
uint32 parentIndex; uint32 parentIndex;
bool countered; address counteredBy;
address claimant;
uint128 bond;
Claim claim; Claim claim;
Position position; Position position;
Clock clock; Clock clock;
......
...@@ -6,5 +6,5 @@ pragma solidity ^0.8.15; ...@@ -6,5 +6,5 @@ pragma solidity ^0.8.15;
interface IInitializable { interface IInitializable {
/// @notice Initializes the contract. /// @notice Initializes the contract.
/// @dev This function may only be called once. /// @dev This function may only be called once.
function initialize() external; function initialize() external payable;
} }
...@@ -27,9 +27,11 @@ error UnexpectedRootClaim(Claim rootClaim); ...@@ -27,9 +27,11 @@ error UnexpectedRootClaim(Claim rootClaim);
/// @notice Thrown when a dispute game has already been initialized. /// @notice Thrown when a dispute game has already been initialized.
error AlreadyInitialized(); error AlreadyInitialized();
/// @notice Thrown when a supplied bond is too low to cover the /// @notice Thrown when a supplied bond is too low to cover the cost of the interaction.
/// cost of the next possible counter claim. error InsufficientBond();
error BondTooLow();
/// @notice Thrown when the transfer of credit to a recipient account reverts.
error BondTransferFailed();
/// @notice Thrown when the `extraData` passed to the CWIA proxy is too long for the `FaultDisputeGame`. /// @notice Thrown when the `extraData` passed to the CWIA proxy is too long for the `FaultDisputeGame`.
error ExtraDataTooLong(); error ExtraDataTooLong();
......
...@@ -51,6 +51,7 @@ abstract contract GameSolver is CommonBase { ...@@ -51,6 +51,7 @@ abstract contract GameSolver is CommonBase {
struct Move { struct Move {
MoveKind kind; MoveKind kind;
bytes data; bytes data;
uint256 value;
} }
constructor( constructor(
...@@ -215,8 +216,12 @@ contract HonestGameSolver is GameSolver { ...@@ -215,8 +216,12 @@ contract HonestGameSolver is GameSolver {
returns (Move memory move_) returns (Move memory move_)
{ {
bool isAttack = _direction == Direction.Attack; bool isAttack = _direction == Direction.Attack;
uint256 bond = GAME.getRequiredBond(_movePos);
move_ = Move({ move_ = Move({
kind: isAttack ? MoveKind.Attack : MoveKind.Defend, kind: isAttack ? MoveKind.Attack : MoveKind.Defend,
value: bond,
data: abi.encodeCall(FaultDisputeGame.move, (_challengeIndex, claimAt(_movePos), isAttack)) data: abi.encodeCall(FaultDisputeGame.move, (_challengeIndex, claimAt(_movePos), isAttack))
}); });
} }
...@@ -256,6 +261,7 @@ contract HonestGameSolver is GameSolver { ...@@ -256,6 +261,7 @@ contract HonestGameSolver is GameSolver {
move_ = Move({ move_ = Move({
kind: MoveKind.Step, kind: MoveKind.Step,
value: 0,
data: abi.encodeCall(FaultDisputeGame.step, (_challengeIndex, isAttack, preStateTrace, hex"")) data: abi.encodeCall(FaultDisputeGame.step, (_challengeIndex, isAttack, preStateTrace, hex""))
}); });
} }
...@@ -268,10 +274,20 @@ contract HonestGameSolver is GameSolver { ...@@ -268,10 +274,20 @@ contract HonestGameSolver is GameSolver {
/// `claimData` array. /// `claimData` array.
function getClaimData(uint256 _claimIndex) internal view returns (IFaultDisputeGame.ClaimData memory claimData_) { function getClaimData(uint256 _claimIndex) internal view returns (IFaultDisputeGame.ClaimData memory claimData_) {
// thanks, solc // thanks, solc
(uint32 parentIndex, bool countered, Claim claim, Position position, Clock clock) = GAME.claimData(_claimIndex); (
uint32 parentIndex,
address countered,
address claimant,
uint128 bond,
Claim claim,
Position position,
Clock clock
) = GAME.claimData(_claimIndex);
claimData_ = IFaultDisputeGame.ClaimData({ claimData_ = IFaultDisputeGame.ClaimData({
parentIndex: parentIndex, parentIndex: parentIndex,
countered: countered, counteredBy: countered,
claimant: claimant,
bond: bond,
claim: claim, claim: claim,
position: position, position: position,
clock: clock clock: clock
...@@ -389,10 +405,14 @@ contract HonestDisputeActor is DisputeActor { ...@@ -389,10 +405,14 @@ contract HonestDisputeActor is DisputeActor {
}); });
} }
(bool innerSuccess,) = address(GAME).call(localMove.data); (bool innerSuccess,) = address(GAME).call{ value: localMove.value }(localMove.data);
assembly { assembly {
success_ := and(success_, innerSuccess) success_ := and(success_, innerSuccess)
} }
} }
} }
fallback() external payable { }
receive() external payable { }
} }
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;
import { Vm } from "forge-std/Vm.sol";
import { StdUtils } from "forge-std/StdUtils.sol";
import { FaultDisputeGame } from "src/dispute/FaultDisputeGame.sol";
import { FaultDisputeGame_Init } from "test/dispute/FaultDisputeGame.t.sol";
import "src/libraries/DisputeTypes.sol";
contract FaultDisputeGame_Solvency_Invariant is FaultDisputeGame_Init {
Claim internal constant ROOT_CLAIM = Claim.wrap(bytes32(uint256(10)));
Claim internal constant ABSOLUTE_PRESTATE = Claim.wrap(bytes32((uint256(3) << 248) | uint256(0)));
RandomClaimActor internal actor;
uint256 internal defaultSenderBalance;
function setUp() public override {
super.setUp();
super.init({
rootClaim: ROOT_CLAIM,
absolutePrestate: ABSOLUTE_PRESTATE,
l2BlockNumber: 0x10,
genesisBlockNumber: 0,
genesisOutputRoot: Hash.wrap(bytes32(0))
});
actor = new RandomClaimActor(gameProxy, vm);
targetContract(address(actor));
vm.startPrank(address(actor));
}
/// @custom:invariant FaultDisputeGame always returns all ETH on total resolution
///
/// The FaultDisputeGame contract should always return all ETH in the contract to the correct recipients upon
/// resolution of all outstanding claims. There may never be any ETH left in the contract after a full resolution.
function invariant_faultDisputeGame_solvency() public {
vm.warp(block.timestamp + 7 days + 1 seconds);
(,,, uint256 rootBond,,,) = gameProxy.claimData(0);
for (uint256 i = gameProxy.claimDataLen(); i > 0; i--) {
(bool success,) = address(gameProxy).call(abi.encodeCall(gameProxy.resolveClaim, (i - 1)));
success;
}
gameProxy.resolve();
gameProxy.claimCredit(address(this));
gameProxy.claimCredit(address(actor));
if (gameProxy.status() == GameStatus.DEFENDER_WINS) {
assertEq(address(this).balance, type(uint96).max);
assertEq(address(actor).balance, actor.totalBonded() - rootBond);
} else if (gameProxy.status() == GameStatus.CHALLENGER_WINS) {
assertEq(DEFAULT_SENDER.balance, type(uint96).max - rootBond);
assertEq(address(actor).balance, actor.totalBonded() + rootBond);
} else {
revert("unreachable");
}
assertEq(address(gameProxy).balance, 0);
}
}
contract RandomClaimActor is StdUtils {
FaultDisputeGame internal immutable GAME;
Vm internal immutable VM;
uint256 public totalBonded;
constructor(FaultDisputeGame _gameProxy, Vm _vm) {
GAME = _gameProxy;
VM = _vm;
}
function move(bool _isAttack, uint256 _parentIndex, Claim _claim, uint64 _bondAmount) public {
_parentIndex = bound(_parentIndex, 0, GAME.claimDataLen());
VM.deal(address(this), _bondAmount);
totalBonded += _bondAmount;
GAME.move{ value: _bondAmount }(_parentIndex, _claim, _isAttack);
}
fallback() external payable { }
receive() external payable { }
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment