Commit 0280a281 authored by Adrian Sutton's avatar Adrian Sutton Committed by GitHub

Use the actual WETH delay when determining when bonds are withdrawable (#10674)

* Use the actual WETH delay when determining when bonds are withdrawable

* Change metric label and logs to reflect using withdrawable timing, not game resolution
parent 59e817b1
...@@ -4,6 +4,7 @@ import ( ...@@ -4,6 +4,7 @@ import (
"context" "context"
"fmt" "fmt"
"math/big" "math/big"
"time"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/contracts/metrics" "github.com/ethereum-optimism/optimism/op-challenger/game/fault/contracts/metrics"
"github.com/ethereum-optimism/optimism/op-service/sources/batching" "github.com/ethereum-optimism/optimism/op-service/sources/batching"
...@@ -14,6 +15,7 @@ import ( ...@@ -14,6 +15,7 @@ import (
var ( var (
methodWithdrawals = "withdrawals" methodWithdrawals = "withdrawals"
methodDelay = "delay"
) )
type DelayedWETHContract struct { type DelayedWETHContract struct {
...@@ -36,6 +38,28 @@ func NewDelayedWETHContract(metrics metrics.ContractMetricer, addr common.Addres ...@@ -36,6 +38,28 @@ func NewDelayedWETHContract(metrics metrics.ContractMetricer, addr common.Addres
} }
} }
func (d *DelayedWETHContract) Addr() common.Address {
return d.contract.Addr()
}
// GetBalanceAndDelay returns the total amount of ETH controlled by this contract and the configured withdrawal delay.
func (d *DelayedWETHContract) GetBalanceAndDelay(ctx context.Context, block rpcblock.Block) (*big.Int, time.Duration, error) {
defer d.metrics.StartContractRequest("GetBalance")()
results, err := d.multiCaller.Call(ctx, block,
batching.NewBalanceCall(d.contract.Addr()),
d.contract.Call(methodDelay))
if err != nil {
return nil, 0, fmt.Errorf("failed to retrieve game balance: %w", err)
}
balance := results[0].GetBigInt(0)
delaySeconds := results[1].GetBigInt(0)
if !delaySeconds.IsInt64() {
return nil, 0, fmt.Errorf("withdrawal delay too big for int64 %v", delaySeconds)
}
delay := time.Duration(delaySeconds.Int64()) * time.Second
return balance, delay, nil
}
// GetWithdrawals returns all withdrawals made from the contract since the given block. // GetWithdrawals returns all withdrawals made from the contract since the given block.
func (d *DelayedWETHContract) GetWithdrawals(ctx context.Context, block rpcblock.Block, gameAddr common.Address, recipients ...common.Address) ([]*WithdrawalRequest, error) { func (d *DelayedWETHContract) GetWithdrawals(ctx context.Context, block rpcblock.Block, gameAddr common.Address, recipients ...common.Address) ([]*WithdrawalRequest, error) {
defer d.metrics.StartContractRequest("GetWithdrawals")() defer d.metrics.StartContractRequest("GetWithdrawals")()
......
...@@ -4,6 +4,7 @@ import ( ...@@ -4,6 +4,7 @@ import (
"context" "context"
"math/big" "math/big"
"testing" "testing"
"time"
contractMetrics "github.com/ethereum-optimism/optimism/op-challenger/game/fault/contracts/metrics" contractMetrics "github.com/ethereum-optimism/optimism/op-challenger/game/fault/contracts/metrics"
"github.com/ethereum-optimism/optimism/op-service/sources/batching" "github.com/ethereum-optimism/optimism/op-service/sources/batching"
...@@ -24,8 +25,8 @@ func TestDelayedWeth_GetWithdrawals(t *testing.T) { ...@@ -24,8 +25,8 @@ func TestDelayedWeth_GetWithdrawals(t *testing.T) {
addrs := []common.Address{{0x01}, {0x02}} addrs := []common.Address{{0x01}, {0x02}}
expected := [][]*big.Int{ expected := [][]*big.Int{
[]*big.Int{big.NewInt(123), big.NewInt(456)}, {big.NewInt(123), big.NewInt(456)},
[]*big.Int{big.NewInt(123), big.NewInt(456)}, {big.NewInt(123), big.NewInt(456)},
} }
for i, addr := range addrs { for i, addr := range addrs {
...@@ -41,6 +42,22 @@ func TestDelayedWeth_GetWithdrawals(t *testing.T) { ...@@ -41,6 +42,22 @@ func TestDelayedWeth_GetWithdrawals(t *testing.T) {
} }
} }
func TestDelayedWeth_GetBalanceAndDelay(t *testing.T) {
stubRpc, weth := setupDelayedWethTest(t)
block := rpcblock.ByNumber(482)
balance := big.NewInt(23984)
delaySeconds := int64(2983294824)
delay := time.Duration(delaySeconds) * time.Second
stubRpc.AddExpectedCall(batchingTest.NewGetBalanceCall(delayedWeth, block, balance))
stubRpc.SetResponse(delayedWeth, methodDelay, block, nil, []interface{}{big.NewInt(delaySeconds)})
actualBalance, actualDelay, err := weth.GetBalanceAndDelay(context.Background(), block)
require.NoError(t, err)
require.Equal(t, balance, actualBalance)
require.Equal(t, delay, actualDelay)
}
func setupDelayedWethTest(t *testing.T) (*batchingTest.AbiBasedRpc, *DelayedWETHContract) { func setupDelayedWethTest(t *testing.T) (*batchingTest.AbiBasedRpc, *DelayedWETHContract) {
delayedWethAbi := snapshots.LoadDelayedWETHABI() delayedWethAbi := snapshots.LoadDelayedWETHABI()
stubRpc := batchingTest.NewAbiBasedRpc(t, delayedWeth, delayedWethAbi) stubRpc := batchingTest.NewAbiBasedRpc(t, delayedWeth, delayedWethAbi)
......
...@@ -136,21 +136,20 @@ func mustParseAbi(json []byte) *abi.ABI { ...@@ -136,21 +136,20 @@ func mustParseAbi(json []byte) *abi.ABI {
return &loaded return &loaded
} }
// GetBalance returns the total amount of ETH controlled by this contract. // GetBalanceAndDelay returns the total amount of ETH controlled by this contract.
// Note that the ETH is actually held by the DelayedWETH contract which may be shared by multiple games. // Note that the ETH is actually held by the DelayedWETH contract which may be shared by multiple games.
// Returns the balance and the address of the contract that actually holds the balance. // Returns the balance and the address of the contract that actually holds the balance.
func (f *FaultDisputeGameContractLatest) GetBalance(ctx context.Context, block rpcblock.Block) (*big.Int, common.Address, error) { func (f *FaultDisputeGameContractLatest) GetBalanceAndDelay(ctx context.Context, block rpcblock.Block) (*big.Int, time.Duration, common.Address, error) {
defer f.metrics.StartContractRequest("GetBalance")() defer f.metrics.StartContractRequest("GetBalanceAndDelay")()
result, err := f.multiCaller.SingleCall(ctx, block, f.contract.Call(methodWETH)) weth, err := f.getDelayedWETH(ctx, block)
if err != nil { if err != nil {
return nil, common.Address{}, fmt.Errorf("failed to load weth address: %w", err) return nil, 0, common.Address{}, fmt.Errorf("failed to get DelayedWETH contract: %w", err)
} }
wethAddr := result.GetAddress(0) balance, delay, err := weth.GetBalanceAndDelay(ctx, block)
result, err = f.multiCaller.SingleCall(ctx, block, batching.NewBalanceCall(wethAddr))
if err != nil { if err != nil {
return nil, common.Address{}, fmt.Errorf("failed to retrieve game balance: %w", err) return nil, 0, common.Address{}, fmt.Errorf("failed to get WETH balance and delay: %w", err)
} }
return result.GetBigInt(0), wethAddr, nil return balance, delay, weth.Addr(), nil
} }
// GetBlockRange returns the block numbers of the absolute pre-state block (typically genesis or the bedrock activation block) // GetBlockRange returns the block numbers of the absolute pre-state block (typically genesis or the bedrock activation block)
...@@ -338,16 +337,16 @@ func (f *FaultDisputeGameContractLatest) addGlobalDataTx(ctx context.Context, da ...@@ -338,16 +337,16 @@ func (f *FaultDisputeGameContractLatest) addGlobalDataTx(ctx context.Context, da
func (f *FaultDisputeGameContractLatest) GetWithdrawals(ctx context.Context, block rpcblock.Block, gameAddr common.Address, recipients ...common.Address) ([]*WithdrawalRequest, error) { func (f *FaultDisputeGameContractLatest) GetWithdrawals(ctx context.Context, block rpcblock.Block, gameAddr common.Address, recipients ...common.Address) ([]*WithdrawalRequest, error) {
defer f.metrics.StartContractRequest("GetWithdrawals")() defer f.metrics.StartContractRequest("GetWithdrawals")()
delayedWETH, err := f.getDelayedWETH(ctx) delayedWETH, err := f.getDelayedWETH(ctx, block)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return delayedWETH.GetWithdrawals(ctx, block, gameAddr, recipients...) return delayedWETH.GetWithdrawals(ctx, block, gameAddr, recipients...)
} }
func (f *FaultDisputeGameContractLatest) getDelayedWETH(ctx context.Context) (*DelayedWETHContract, error) { func (f *FaultDisputeGameContractLatest) getDelayedWETH(ctx context.Context, block rpcblock.Block) (*DelayedWETHContract, error) {
defer f.metrics.StartContractRequest("GetDelayedWETH")() defer f.metrics.StartContractRequest("GetDelayedWETH")()
result, err := f.multiCaller.SingleCall(ctx, rpcblock.Latest, f.contract.Call(methodWETH)) result, err := f.multiCaller.SingleCall(ctx, block, f.contract.Call(methodWETH))
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to fetch WETH addr: %w", err) return nil, fmt.Errorf("failed to fetch WETH addr: %w", err)
} }
...@@ -593,7 +592,7 @@ func (f *FaultDisputeGameContractLatest) decodeClaim(result *batching.CallResult ...@@ -593,7 +592,7 @@ func (f *FaultDisputeGameContractLatest) decodeClaim(result *batching.CallResult
} }
type FaultDisputeGameContract interface { type FaultDisputeGameContract interface {
GetBalance(ctx context.Context, block rpcblock.Block) (*big.Int, common.Address, error) GetBalanceAndDelay(ctx context.Context, block rpcblock.Block) (*big.Int, time.Duration, common.Address, error)
GetBlockRange(ctx context.Context) (prestateBlock uint64, poststateBlock uint64, retErr error) GetBlockRange(ctx context.Context) (prestateBlock uint64, poststateBlock uint64, retErr error)
GetGameMetadata(ctx context.Context, block rpcblock.Block) (GameMetadata, error) GetGameMetadata(ctx context.Context, block rpcblock.Block) (GameMetadata, error)
GetStartingRootHash(ctx context.Context) (common.Hash, error) GetStartingRootHash(ctx context.Context) (common.Hash, error)
......
...@@ -327,14 +327,19 @@ func TestGetBalance(t *testing.T) { ...@@ -327,14 +327,19 @@ func TestGetBalance(t *testing.T) {
t.Run(version.version, func(t *testing.T) { t.Run(version.version, func(t *testing.T) {
wethAddr := common.Address{0x11, 0x55, 0x66} wethAddr := common.Address{0x11, 0x55, 0x66}
balance := big.NewInt(9995877) balance := big.NewInt(9995877)
delaySeconds := big.NewInt(429829)
delay := time.Duration(delaySeconds.Int64()) * time.Second
block := rpcblock.ByNumber(424) block := rpcblock.ByNumber(424)
stubRpc, game := setupFaultDisputeGameTest(t, version) stubRpc, game := setupFaultDisputeGameTest(t, version)
stubRpc.SetResponse(fdgAddr, methodWETH, block, nil, []interface{}{wethAddr}) stubRpc.SetResponse(fdgAddr, methodWETH, block, nil, []interface{}{wethAddr})
stubRpc.AddContract(wethAddr, snapshots.LoadDelayedWETHABI())
stubRpc.SetResponse(wethAddr, methodDelay, block, nil, []interface{}{delaySeconds})
stubRpc.AddExpectedCall(batchingTest.NewGetBalanceCall(wethAddr, block, balance)) stubRpc.AddExpectedCall(batchingTest.NewGetBalanceCall(wethAddr, block, balance))
actualBalance, actualAddr, err := game.GetBalance(context.Background(), block) actualBalance, actualDelay, actualAddr, err := game.GetBalanceAndDelay(context.Background(), block)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, wethAddr, actualAddr) require.Equal(t, wethAddr, actualAddr)
require.Equal(t, delay, actualDelay)
require.Truef(t, balance.Cmp(actualBalance) == 0, "Expected balance %v but was %v", balance, actualBalance) require.Truef(t, balance.Cmp(actualBalance) == 0, "Expected balance %v but was %v", balance, actualBalance)
}) })
} }
......
...@@ -41,14 +41,14 @@ type CreditExpectation uint8 ...@@ -41,14 +41,14 @@ type CreditExpectation uint8
const ( const (
// Max Duration reached // Max Duration reached
CreditBelowMaxDuration CreditExpectation = iota CreditBelowWithdrawable CreditExpectation = iota
CreditEqualMaxDuration CreditEqualWithdrawable
CreditAboveMaxDuration CreditAboveWithdrawable
// Max Duration not reached // Max Duration not reached
CreditBelowNonMaxDuration CreditBelowNonWithdrawable
CreditEqualNonMaxDuration CreditEqualNonWithdrawable
CreditAboveNonMaxDuration CreditAboveNonWithdrawable
) )
type GameAgreementStatus uint8 type GameAgreementStatus uint8
...@@ -293,7 +293,7 @@ func NewMetrics() *Metrics { ...@@ -293,7 +293,7 @@ func NewMetrics() *Metrics {
Help: "Cumulative credits", Help: "Cumulative credits",
}, []string{ }, []string{
"credit", "credit",
"max_duration", "withdrawable",
}), }),
claims: *factory.NewGaugeVec(prometheus.GaugeOpts{ claims: *factory.NewGaugeVec(prometheus.GaugeOpts{
Namespace: Namespace, Namespace: Namespace,
...@@ -434,18 +434,18 @@ func (m *Metrics) RecordGameResolutionStatus(status ResolutionStatus, count int) ...@@ -434,18 +434,18 @@ func (m *Metrics) RecordGameResolutionStatus(status ResolutionStatus, count int)
func (m *Metrics) RecordCredit(expectation CreditExpectation, count int) { func (m *Metrics) RecordCredit(expectation CreditExpectation, count int) {
asLabels := func(expectation CreditExpectation) []string { asLabels := func(expectation CreditExpectation) []string {
switch expectation { switch expectation {
case CreditBelowMaxDuration: case CreditBelowWithdrawable:
return []string{"below", "max_duration"} return []string{"below", "withdrawable"}
case CreditEqualMaxDuration: case CreditEqualWithdrawable:
return []string{"expected", "max_duration"} return []string{"expected", "withdrawable"}
case CreditAboveMaxDuration: case CreditAboveWithdrawable:
return []string{"above", "max_duration"} return []string{"above", "withdrawable"}
case CreditBelowNonMaxDuration: case CreditBelowNonWithdrawable:
return []string{"below", "non_max_duration"} return []string{"below", "non_withdrawable"}
case CreditEqualNonMaxDuration: case CreditEqualNonWithdrawable:
return []string{"expected", "non_max_duration"} return []string{"expected", "non_withdrawable"}
case CreditAboveNonMaxDuration: case CreditAboveNonWithdrawable:
return []string{"above", "non_max_duration"} return []string{"above", "non_withdrawable"}
default: default:
panic(fmt.Errorf("unknown credit expectation: %v", expectation)) panic(fmt.Errorf("unknown credit expectation: %v", expectation))
} }
......
...@@ -51,7 +51,7 @@ func (b *Bonds) checkCredits(games []*types.EnrichedGameData) { ...@@ -51,7 +51,7 @@ func (b *Bonds) checkCredits(games []*types.EnrichedGameData) {
for _, game := range games { for _, game := range games {
// Check if the max duration has been reached for this game // Check if the max duration has been reached for this game
duration := uint64(b.clock.Now().Unix()) - game.Timestamp duration := uint64(b.clock.Now().Unix()) - game.Timestamp
maxDurationReached := duration >= game.MaxClockDuration*2 maxDurationReached := duration >= game.MaxClockDuration+uint64(game.WETHDelay.Seconds())
// Iterate over claims, filter out resolved ones and sum up expected credits per recipient // Iterate over claims, filter out resolved ones and sum up expected credits per recipient
expectedCredits := make(map[common.Address]*big.Int) expectedCredits := make(map[common.Address]*big.Int)
...@@ -95,32 +95,32 @@ func (b *Bonds) checkCredits(games []*types.EnrichedGameData) { ...@@ -95,32 +95,32 @@ func (b *Bonds) checkCredits(games []*types.EnrichedGameData) {
comparison := actual.Cmp(expected) comparison := actual.Cmp(expected)
if maxDurationReached { if maxDurationReached {
if comparison > 0 { if comparison > 0 {
creditMetrics[metrics.CreditAboveMaxDuration] += 1 creditMetrics[metrics.CreditAboveWithdrawable] += 1
b.logger.Warn("Credit above expected amount", "recipient", recipient, "expected", expected, "actual", actual, "game", game.Proxy, "duration", "reached") b.logger.Warn("Credit above expected amount", "recipient", recipient, "expected", expected, "actual", actual, "game", game.Proxy, "withdrawable", "withdrawable")
} else if comparison == 0 { } else if comparison == 0 {
creditMetrics[metrics.CreditEqualMaxDuration] += 1 creditMetrics[metrics.CreditEqualWithdrawable] += 1
} else { } else {
creditMetrics[metrics.CreditBelowMaxDuration] += 1 creditMetrics[metrics.CreditBelowWithdrawable] += 1
} }
} else { } else {
if comparison > 0 { if comparison > 0 {
creditMetrics[metrics.CreditAboveNonMaxDuration] += 1 creditMetrics[metrics.CreditAboveNonWithdrawable] += 1
b.logger.Warn("Credit above expected amount", "recipient", recipient, "expected", expected, "actual", actual, "game", game.Proxy, "duration", "unreached") b.logger.Warn("Credit above expected amount", "recipient", recipient, "expected", expected, "actual", actual, "game", game.Proxy, "withdrawable", "non_withdrawable")
} else if comparison == 0 { } else if comparison == 0 {
creditMetrics[metrics.CreditEqualNonMaxDuration] += 1 creditMetrics[metrics.CreditEqualNonWithdrawable] += 1
} else { } else {
creditMetrics[metrics.CreditBelowNonMaxDuration] += 1 creditMetrics[metrics.CreditBelowNonWithdrawable] += 1
b.logger.Warn("Credit withdrawn early", "recipient", recipient, "expected", expected, "actual", actual, "game", game.Proxy, "duration", "unreached") b.logger.Error("Credit withdrawn early", "recipient", recipient, "expected", expected, "actual", actual, "game", game.Proxy, "withdrawable", "non_withdrawable")
} }
} }
} }
} }
b.metrics.RecordCredit(metrics.CreditBelowMaxDuration, creditMetrics[metrics.CreditBelowMaxDuration]) b.metrics.RecordCredit(metrics.CreditBelowWithdrawable, creditMetrics[metrics.CreditBelowWithdrawable])
b.metrics.RecordCredit(metrics.CreditEqualMaxDuration, creditMetrics[metrics.CreditEqualMaxDuration]) b.metrics.RecordCredit(metrics.CreditEqualWithdrawable, creditMetrics[metrics.CreditEqualWithdrawable])
b.metrics.RecordCredit(metrics.CreditAboveMaxDuration, creditMetrics[metrics.CreditAboveMaxDuration]) b.metrics.RecordCredit(metrics.CreditAboveWithdrawable, creditMetrics[metrics.CreditAboveWithdrawable])
b.metrics.RecordCredit(metrics.CreditBelowNonMaxDuration, creditMetrics[metrics.CreditBelowNonMaxDuration]) b.metrics.RecordCredit(metrics.CreditBelowNonWithdrawable, creditMetrics[metrics.CreditBelowNonWithdrawable])
b.metrics.RecordCredit(metrics.CreditEqualNonMaxDuration, creditMetrics[metrics.CreditEqualNonMaxDuration]) b.metrics.RecordCredit(metrics.CreditEqualNonWithdrawable, creditMetrics[metrics.CreditEqualNonWithdrawable])
b.metrics.RecordCredit(metrics.CreditAboveNonMaxDuration, creditMetrics[metrics.CreditAboveNonMaxDuration]) b.metrics.RecordCredit(metrics.CreditAboveNonWithdrawable, creditMetrics[metrics.CreditAboveNonWithdrawable])
} }
...@@ -69,6 +69,7 @@ func TestCheckRecipientCredit(t *testing.T) { ...@@ -69,6 +69,7 @@ func TestCheckRecipientCredit(t *testing.T) {
// Game has not reached max duration // Game has not reached max duration
game1 := &monTypes.EnrichedGameData{ game1 := &monTypes.EnrichedGameData{
MaxClockDuration: 50000, MaxClockDuration: 50000,
WETHDelay: 30 * time.Minute,
GameMetadata: gameTypes.GameMetadata{ GameMetadata: gameTypes.GameMetadata{
Proxy: common.Address{0x11}, Proxy: common.Address{0x11},
Timestamp: uint64(frozen.Unix()), Timestamp: uint64(frozen.Unix()),
...@@ -141,6 +142,7 @@ func TestCheckRecipientCredit(t *testing.T) { ...@@ -141,6 +142,7 @@ func TestCheckRecipientCredit(t *testing.T) {
// Max duration has been reached // Max duration has been reached
game2 := &monTypes.EnrichedGameData{ game2 := &monTypes.EnrichedGameData{
MaxClockDuration: 5, MaxClockDuration: 5,
WETHDelay: 5 * time.Second,
GameMetadata: gameTypes.GameMetadata{ GameMetadata: gameTypes.GameMetadata{
Proxy: common.Address{0x22}, Proxy: common.Address{0x22},
Timestamp: uint64(frozen.Unix()) - 11, Timestamp: uint64(frozen.Unix()) - 11,
...@@ -215,6 +217,7 @@ func TestCheckRecipientCredit(t *testing.T) { ...@@ -215,6 +217,7 @@ func TestCheckRecipientCredit(t *testing.T) {
// Game has not reached max duration // Game has not reached max duration
game3 := &monTypes.EnrichedGameData{ game3 := &monTypes.EnrichedGameData{
MaxClockDuration: 50000, MaxClockDuration: 50000,
WETHDelay: 10 * time.Hour,
GameMetadata: gameTypes.GameMetadata{ GameMetadata: gameTypes.GameMetadata{
Proxy: common.Address{0x33}, Proxy: common.Address{0x33},
Timestamp: uint64(frozen.Unix()) - 11, Timestamp: uint64(frozen.Unix()) - 11,
...@@ -268,6 +271,7 @@ func TestCheckRecipientCredit(t *testing.T) { ...@@ -268,6 +271,7 @@ func TestCheckRecipientCredit(t *testing.T) {
// Game has not reached max duration // Game has not reached max duration
game4 := &monTypes.EnrichedGameData{ game4 := &monTypes.EnrichedGameData{
MaxClockDuration: 10, MaxClockDuration: 10,
WETHDelay: 10 * time.Second,
GameMetadata: gameTypes.GameMetadata{ GameMetadata: gameTypes.GameMetadata{
Proxy: common.Address{44}, Proxy: common.Address{44},
Timestamp: uint64(frozen.Unix()) - 22, Timestamp: uint64(frozen.Unix()) - 22,
...@@ -325,46 +329,46 @@ func TestCheckRecipientCredit(t *testing.T) { ...@@ -325,46 +329,46 @@ func TestCheckRecipientCredit(t *testing.T) {
bonds.CheckBonds([]*monTypes.EnrichedGameData{game1, game2, game3, game4}) bonds.CheckBonds([]*monTypes.EnrichedGameData{game1, game2, game3, game4})
require.Len(t, m.credits, 6) require.Len(t, m.credits, 6)
require.Contains(t, m.credits, metrics.CreditBelowMaxDuration) require.Contains(t, m.credits, metrics.CreditBelowWithdrawable)
require.Contains(t, m.credits, metrics.CreditEqualMaxDuration) require.Contains(t, m.credits, metrics.CreditEqualWithdrawable)
require.Contains(t, m.credits, metrics.CreditAboveMaxDuration) require.Contains(t, m.credits, metrics.CreditAboveWithdrawable)
require.Contains(t, m.credits, metrics.CreditBelowNonMaxDuration) require.Contains(t, m.credits, metrics.CreditBelowNonWithdrawable)
require.Contains(t, m.credits, metrics.CreditEqualNonMaxDuration) require.Contains(t, m.credits, metrics.CreditEqualNonWithdrawable)
require.Contains(t, m.credits, metrics.CreditAboveNonMaxDuration) require.Contains(t, m.credits, metrics.CreditAboveNonWithdrawable)
// Game 2 and 4 recipients added here as it has reached max duration // Game 2 and 4 recipients added here as it has reached max duration
require.Equal(t, 2, m.credits[metrics.CreditBelowMaxDuration], "CreditBelowMaxDuration") require.Equal(t, 2, m.credits[metrics.CreditBelowWithdrawable], "CreditBelowWithdrawable")
require.Equal(t, 3, m.credits[metrics.CreditEqualMaxDuration], "CreditEqualMaxDuration") require.Equal(t, 3, m.credits[metrics.CreditEqualWithdrawable], "CreditEqualWithdrawable")
require.Equal(t, 2, m.credits[metrics.CreditAboveMaxDuration], "CreditAboveMaxDuration") require.Equal(t, 2, m.credits[metrics.CreditAboveWithdrawable], "CreditAboveWithdrawable")
// Game 1 and 3 recipients added here as it hasn't reached max duration // Game 1 and 3 recipients added here as it hasn't reached max duration
require.Equal(t, 3, m.credits[metrics.CreditBelowNonMaxDuration], "CreditBelowNonMaxDuration") require.Equal(t, 3, m.credits[metrics.CreditBelowNonWithdrawable], "CreditBelowNonWithdrawable")
require.Equal(t, 2, m.credits[metrics.CreditEqualNonMaxDuration], "CreditEqualNonMaxDuration") require.Equal(t, 2, m.credits[metrics.CreditEqualNonWithdrawable], "CreditEqualNonWithdrawable")
require.Equal(t, 2, m.credits[metrics.CreditAboveNonMaxDuration], "CreditAboveNonMaxDuration") require.Equal(t, 2, m.credits[metrics.CreditAboveNonWithdrawable], "CreditAboveNonWithdrawable")
// Logs from game1 // Logs from game1
// addr1 is correct so has no logs // addr1 is correct so has no logs
// addr2 is below expected before max duration, so warn about early withdrawal // addr2 is below expected before max duration, so warn about early withdrawal
require.NotNil(t, logs.FindLog( require.NotNil(t, logs.FindLog(
testlog.NewLevelFilter(log.LevelWarn), testlog.NewLevelFilter(log.LevelError),
testlog.NewMessageFilter("Credit withdrawn early"), testlog.NewMessageFilter("Credit withdrawn early"),
testlog.NewAttributesFilter("game", game1.Proxy.Hex()), testlog.NewAttributesFilter("game", game1.Proxy.Hex()),
testlog.NewAttributesFilter("recipient", addr2.Hex()), testlog.NewAttributesFilter("recipient", addr2.Hex()),
testlog.NewAttributesFilter("duration", "unreached"))) testlog.NewAttributesFilter("withdrawable", "non_withdrawable")))
// addr3 is above expected // addr3 is above expected
require.NotNil(t, logs.FindLog( require.NotNil(t, logs.FindLog(
testlog.NewLevelFilter(log.LevelWarn), testlog.NewLevelFilter(log.LevelWarn),
testlog.NewMessageFilter("Credit above expected amount"), testlog.NewMessageFilter("Credit above expected amount"),
testlog.NewAttributesFilter("game", game1.Proxy.Hex()), testlog.NewAttributesFilter("game", game1.Proxy.Hex()),
testlog.NewAttributesFilter("recipient", addr3.Hex()), testlog.NewAttributesFilter("recipient", addr3.Hex()),
testlog.NewAttributesFilter("duration", "unreached"))) testlog.NewAttributesFilter("withdrawable", "non_withdrawable")))
// addr4 is below expected before max duration, so warn about early withdrawal // addr4 is below expected before max duration, so warn about early withdrawal
require.NotNil(t, logs.FindLog( require.NotNil(t, logs.FindLog(
testlog.NewLevelFilter(log.LevelWarn), testlog.NewLevelFilter(log.LevelError),
testlog.NewMessageFilter("Credit withdrawn early"), testlog.NewMessageFilter("Credit withdrawn early"),
testlog.NewAttributesFilter("game", game1.Proxy.Hex()), testlog.NewAttributesFilter("game", game1.Proxy.Hex()),
testlog.NewAttributesFilter("recipient", addr4.Hex()), testlog.NewAttributesFilter("recipient", addr4.Hex()),
testlog.NewAttributesFilter("duration", "unreached"))) testlog.NewAttributesFilter("withdrawable", "non_withdrawable")))
// Logs from game 2 // Logs from game 2
// addr1 is below expected - no warning as withdrawals may now be possible // addr1 is below expected - no warning as withdrawals may now be possible
...@@ -375,18 +379,18 @@ func TestCheckRecipientCredit(t *testing.T) { ...@@ -375,18 +379,18 @@ func TestCheckRecipientCredit(t *testing.T) {
testlog.NewMessageFilter("Credit above expected amount"), testlog.NewMessageFilter("Credit above expected amount"),
testlog.NewAttributesFilter("game", game2.Proxy.Hex()), testlog.NewAttributesFilter("game", game2.Proxy.Hex()),
testlog.NewAttributesFilter("recipient", addr3.Hex()), testlog.NewAttributesFilter("recipient", addr3.Hex()),
testlog.NewAttributesFilter("duration", "reached"))) testlog.NewAttributesFilter("withdrawable", "withdrawable")))
// addr4 is correct // addr4 is correct
// Logs from game 3 // Logs from game 3
// addr1 is correct so has no logs // addr1 is correct so has no logs
// addr2 is below expected before max duration, so warn about early withdrawal // addr2 is below expected before max duration, so warn about early withdrawal
require.NotNil(t, logs.FindLog( require.NotNil(t, logs.FindLog(
testlog.NewLevelFilter(log.LevelWarn), testlog.NewLevelFilter(log.LevelError),
testlog.NewMessageFilter("Credit withdrawn early"), testlog.NewMessageFilter("Credit withdrawn early"),
testlog.NewAttributesFilter("game", game3.Proxy.Hex()), testlog.NewAttributesFilter("game", game3.Proxy.Hex()),
testlog.NewAttributesFilter("recipient", addr2.Hex()), testlog.NewAttributesFilter("recipient", addr2.Hex()),
testlog.NewAttributesFilter("duration", "unreached"))) testlog.NewAttributesFilter("withdrawable", "non_withdrawable")))
// addr3 is not involved so no logs // addr3 is not involved so no logs
// addr4 is above expected before max duration, so warn // addr4 is above expected before max duration, so warn
require.NotNil(t, logs.FindLog( require.NotNil(t, logs.FindLog(
...@@ -394,7 +398,7 @@ func TestCheckRecipientCredit(t *testing.T) { ...@@ -394,7 +398,7 @@ func TestCheckRecipientCredit(t *testing.T) {
testlog.NewMessageFilter("Credit above expected amount"), testlog.NewMessageFilter("Credit above expected amount"),
testlog.NewAttributesFilter("game", game3.Proxy.Hex()), testlog.NewAttributesFilter("game", game3.Proxy.Hex()),
testlog.NewAttributesFilter("recipient", addr4.Hex()), testlog.NewAttributesFilter("recipient", addr4.Hex()),
testlog.NewAttributesFilter("duration", "unreached"))) testlog.NewAttributesFilter("withdrawable", "non_withdrawable")))
// Logs from game 4 // Logs from game 4
// addr1 is correct so has no logs // addr1 is correct so has no logs
...@@ -406,7 +410,7 @@ func TestCheckRecipientCredit(t *testing.T) { ...@@ -406,7 +410,7 @@ func TestCheckRecipientCredit(t *testing.T) {
testlog.NewMessageFilter("Credit above expected amount"), testlog.NewMessageFilter("Credit above expected amount"),
testlog.NewAttributesFilter("game", game4.Proxy.Hex()), testlog.NewAttributesFilter("game", game4.Proxy.Hex()),
testlog.NewAttributesFilter("recipient", addr4.Hex()), testlog.NewAttributesFilter("recipient", addr4.Hex()),
testlog.NewAttributesFilter("duration", "reached"))) testlog.NewAttributesFilter("withdrawable", "withdrawable")))
} }
func setupBondMetricsTest(t *testing.T) (*Bonds, *stubBondMetrics, *testlog.CapturingHandler) { func setupBondMetricsTest(t *testing.T) (*Bonds, *stubBondMetrics, *testlog.CapturingHandler) {
......
...@@ -4,6 +4,7 @@ import ( ...@@ -4,6 +4,7 @@ import (
"context" "context"
"fmt" "fmt"
"math/big" "math/big"
"time"
monTypes "github.com/ethereum-optimism/optimism/op-dispute-mon/mon/types" monTypes "github.com/ethereum-optimism/optimism/op-dispute-mon/mon/types"
"github.com/ethereum-optimism/optimism/op-service/sources/batching/rpcblock" "github.com/ethereum-optimism/optimism/op-service/sources/batching/rpcblock"
...@@ -13,7 +14,7 @@ import ( ...@@ -13,7 +14,7 @@ import (
var _ Enricher = (*BalanceEnricher)(nil) var _ Enricher = (*BalanceEnricher)(nil)
type BalanceCaller interface { type BalanceCaller interface {
GetBalance(context.Context, rpcblock.Block) (*big.Int, common.Address, error) GetBalanceAndDelay(context.Context, rpcblock.Block) (*big.Int, time.Duration, common.Address, error)
} }
type BalanceEnricher struct{} type BalanceEnricher struct{}
...@@ -23,11 +24,12 @@ func NewBalanceEnricher() *BalanceEnricher { ...@@ -23,11 +24,12 @@ func NewBalanceEnricher() *BalanceEnricher {
} }
func (b *BalanceEnricher) Enrich(ctx context.Context, block rpcblock.Block, caller GameCaller, game *monTypes.EnrichedGameData) error { func (b *BalanceEnricher) Enrich(ctx context.Context, block rpcblock.Block, caller GameCaller, game *monTypes.EnrichedGameData) error {
balance, holdingAddr, err := caller.GetBalance(ctx, block) balance, delay, holdingAddr, err := caller.GetBalanceAndDelay(ctx, block)
if err != nil { if err != nil {
return fmt.Errorf("failed to fetch balance: %w", err) return fmt.Errorf("failed to fetch balance: %w", err)
} }
game.ETHCollateral = balance game.ETHCollateral = balance
game.WETHContract = holdingAddr game.WETHContract = holdingAddr
game.WETHDelay = delay
return nil return nil
} }
...@@ -5,6 +5,7 @@ import ( ...@@ -5,6 +5,7 @@ import (
"errors" "errors"
"math/big" "math/big"
"testing" "testing"
"time"
"github.com/ethereum-optimism/optimism/op-dispute-mon/mon/types" "github.com/ethereum-optimism/optimism/op-dispute-mon/mon/types"
"github.com/ethereum-optimism/optimism/op-service/sources/batching/rpcblock" "github.com/ethereum-optimism/optimism/op-service/sources/batching/rpcblock"
...@@ -23,11 +24,16 @@ func TestBalanceEnricher(t *testing.T) { ...@@ -23,11 +24,16 @@ func TestBalanceEnricher(t *testing.T) {
t.Run("GetBalanceSuccess", func(t *testing.T) { t.Run("GetBalanceSuccess", func(t *testing.T) {
enricher := NewBalanceEnricher() enricher := NewBalanceEnricher()
caller := &mockGameCaller{balance: big.NewInt(84242), balanceAddr: common.Address{0xdd}} caller := &mockGameCaller{
balance: big.NewInt(84242),
delayDuration: 3 * time.Hour,
balanceAddr: common.Address{0xdd},
}
game := &types.EnrichedGameData{} game := &types.EnrichedGameData{}
err := enricher.Enrich(context.Background(), rpcblock.Latest, caller, game) err := enricher.Enrich(context.Background(), rpcblock.Latest, caller, game)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, game.WETHContract, caller.balanceAddr) require.Equal(t, game.WETHContract, caller.balanceAddr)
require.Equal(t, game.ETHCollateral, caller.balance) require.Equal(t, game.ETHCollateral, caller.balance)
require.Equal(t, game.WETHDelay, caller.delayDuration)
}) })
} }
...@@ -5,6 +5,7 @@ import ( ...@@ -5,6 +5,7 @@ import (
"errors" "errors"
"math/big" "math/big"
"testing" "testing"
"time"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/contracts" "github.com/ethereum-optimism/optimism/op-challenger/game/fault/contracts"
monTypes "github.com/ethereum-optimism/optimism/op-dispute-mon/mon/types" monTypes "github.com/ethereum-optimism/optimism/op-dispute-mon/mon/types"
...@@ -226,6 +227,7 @@ type mockGameCaller struct { ...@@ -226,6 +227,7 @@ type mockGameCaller struct {
extraCredit []*big.Int extraCredit []*big.Int
balanceErr error balanceErr error
balance *big.Int balance *big.Int
delayDuration time.Duration
balanceAddr common.Address balanceAddr common.Address
withdrawalsCalls int withdrawalsCalls int
withdrawalsErr error withdrawalsErr error
...@@ -290,11 +292,11 @@ func (m *mockGameCaller) GetCredits(_ context.Context, _ rpcblock.Block, recipie ...@@ -290,11 +292,11 @@ func (m *mockGameCaller) GetCredits(_ context.Context, _ rpcblock.Block, recipie
return response, nil return response, nil
} }
func (m *mockGameCaller) GetBalance(_ context.Context, _ rpcblock.Block) (*big.Int, common.Address, error) { func (m *mockGameCaller) GetBalanceAndDelay(_ context.Context, _ rpcblock.Block) (*big.Int, time.Duration, common.Address, error) {
if m.balanceErr != nil { if m.balanceErr != nil {
return nil, common.Address{}, m.balanceErr return nil, 0, common.Address{}, m.balanceErr
} }
return m.balance, m.balanceAddr, nil return m.balance, m.delayDuration, m.balanceAddr, nil
} }
func (m *mockGameCaller) IsResolved(_ context.Context, _ rpcblock.Block, claims ...faultTypes.Claim) ([]bool, error) { func (m *mockGameCaller) IsResolved(_ context.Context, _ rpcblock.Block, claims ...faultTypes.Claim) ([]bool, error) {
......
...@@ -2,6 +2,7 @@ package types ...@@ -2,6 +2,7 @@ package types
import ( import (
"math/big" "math/big"
"time"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/contracts" "github.com/ethereum-optimism/optimism/op-challenger/game/fault/contracts"
faultTypes "github.com/ethereum-optimism/optimism/op-challenger/game/fault/types" faultTypes "github.com/ethereum-optimism/optimism/op-challenger/game/fault/types"
...@@ -43,6 +44,9 @@ type EnrichedGameData struct { ...@@ -43,6 +44,9 @@ type EnrichedGameData struct {
// The contract is potentially shared by multiple games. // The contract is potentially shared by multiple games.
WETHContract common.Address WETHContract common.Address
// WETHDelay is the delay applied before credits can be withdrawn.
WETHDelay time.Duration
// ETHCollateral is the ETH balance of the (potentially shared) WETHContract // ETHCollateral is the ETH balance of the (potentially shared) WETHContract
// This ETH balance will be used to pay out any bonds required by the games // This ETH balance will be used to pay out any bonds required by the games
// that use the same DelayedWETH contract. // that use the same DelayedWETH contract.
......
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