diff --git a/.gitignore b/.gitignore index 1d3760f9..807b6a40 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,4 @@ chart_releases /snapshots/**/*.csv *.sql *.dump +.env \ No newline at end of file diff --git a/Makefile b/Makefile index fba5743e..740d7793 100644 --- a/Makefile +++ b/Makefile @@ -84,6 +84,15 @@ lint: test: ./scripts/goTest.sh -v -p 1 -parallel 1 ./... +.PHONY: test-file +test-file: + @if [ -z "$(FILE)" ]; then \ + echo "Error: FILE variable is not set."; \ + echo "Usage: make test-file FILE=path/to/your_test_file.go"; \ + exit 1; \ + fi + ./scripts/goTest.sh -v -p 1 -parallel 1 $(FILE) + .PHONY: staticcheck staticcheck: staticcheck ./... diff --git a/pkg/eigenState/stakerShares/stakerShares.go b/pkg/eigenState/stakerShares/stakerShares.go index 990fb211..7aabeb9d 100644 --- a/pkg/eigenState/stakerShares/stakerShares.go +++ b/pkg/eigenState/stakerShares/stakerShares.go @@ -357,6 +357,62 @@ func (ss *StakerSharesModel) handleM2QueuedWithdrawal(log *storage.TransactionLo return records, nil } +type slashingWithdrawalQueuedOutputData struct { + Withdrawal struct { + Nonce int `json:"nonce"` + ScaledShares []json.Number `json:"scaledShares"` + Staker string `json:"staker"` + StartBlock uint64 `json:"startBlock"` + Strategies []string `json:"strategies"` + } `json:"withdrawal"` + WithdrawalRoot []byte `json:"withdrawalRoot"` + SharesToWithdraw []json.Number `json:"sharesToWithdraw"` + WithdrawalRootString string +} + +func parseLogOutputForSlashingWithdrawalQueuedEvent(outputDataStr string) (*slashingWithdrawalQueuedOutputData, error) { + outputData := &slashingWithdrawalQueuedOutputData{} + decoder := json.NewDecoder(strings.NewReader(outputDataStr)) + decoder.UseNumber() + + err := decoder.Decode(&outputData) + if err != nil { + return nil, err + } + outputData.Withdrawal.Staker = strings.ToLower(outputData.Withdrawal.Staker) + outputData.WithdrawalRootString = hex.EncodeToString(outputData.WithdrawalRoot) + return outputData, err +} + +// handleSlashingWithdrawalQueued handles the WithdrawalQueued event from the DelegationManager contract for slashing +func (ss *StakerSharesModel) handleSlashingWithdrawalQueued(log *storage.TransactionLog) ([]*StakerShareDeltas, error) { + // do the same thing as handleM2QueuedWithdrawal + outputData, err := parseLogOutputForSlashingWithdrawalQueuedEvent(log.OutputData) + if err != nil { + return nil, err + } + + records := make([]*StakerShareDeltas, 0) + for i, strategy := range outputData.Withdrawal.Strategies { + shares, success := numbers.NewBig257().SetString(outputData.SharesToWithdraw[i].String(), 10) + if !success { + return nil, xerrors.Errorf("Failed to convert shares to big.Int: %s", outputData.SharesToWithdraw[i]) + } + r := &StakerShareDeltas{ + Staker: outputData.Withdrawal.Staker, + Strategy: strategy, + Shares: shares.Mul(shares, big.NewInt(-1)).String(), + StrategyIndex: uint64(i), + LogIndex: log.LogIndex, + TransactionHash: log.TransactionHash, + BlockNumber: log.BlockNumber, + WithdrawalRootString: outputData.WithdrawalRootString, + } + records = append(records, r) + } + return records, nil +} + type AccumulatedStateChanges struct { Changes []*StakerShareDeltas } @@ -436,6 +492,11 @@ func (ss *StakerSharesModel) GetStateTransitions() (types.StateTransitions[*Accu ss.stateAccumulator[log.BlockNumber] = filteredDeltas } } + } else if log.Address == contractAddresses.DelegationManager && log.EventName == "SlashingWithdrawalQueued" { + records, err := ss.handleSlashingWithdrawalQueued(log) + if err == nil && records != nil { + deltaRecords = append(deltaRecords, records...) + } } else { ss.logger.Sugar().Debugw("Got stakerShares event that we don't handle", zap.String("eventName", log.EventName), diff --git a/pkg/eigenState/stakerShares/stakerShares_test.go b/pkg/eigenState/stakerShares/stakerShares_test.go index 48fbc66f..eb3444f2 100644 --- a/pkg/eigenState/stakerShares/stakerShares_test.go +++ b/pkg/eigenState/stakerShares/stakerShares_test.go @@ -456,6 +456,84 @@ func Test_StakerSharesState(t *testing.T) { } assert.Equal(t, 4, count) }) + + t.Run("Should capture Slashing withdrawals", func(t *testing.T) { + esm := stateManager.NewEigenStateManager(l, grm) + blockNumber := uint64(200) + log := storage.TransactionLog{ + TransactionHash: "some hash", + TransactionIndex: big.NewInt(300).Uint64(), + BlockNumber: blockNumber, + Address: cfg.GetContractsMapForChain().DelegationManager, + Arguments: `[{"Name": "withdrawalRoot", "Type": "bytes32", "Value": ""}, {"Name": "withdrawal", "Type": "(address,address,address,uint256,uint32,address[],uint256[])", "Value": ""}]`, + EventName: "SlashingWithdrawalQueued", + LogIndex: big.NewInt(600).Uint64(), + OutputData: `{"withdrawal": {"nonce": 0, "scaledShares": [1000000000000000000], "staker": "0x3c42cd72639e3e8d11ab8d0072cc13bd5d8aa83c", "startBlock": 1215690, "strategies": ["0xd523267698c81a372191136e477fdebfa33d9fb4"], "withdrawer": "0x3c42cd72639e3e8d11ab8d0072cc13bd5d8aa83c", "delegatedTo": "0x2177dee1f66d6dbfbf517d9c4f316024c6a21aeb"}, "withdrawalRoot": [24, 23, 49, 137, 14, 63, 119, 12, 234, 225, 63, 35, 109, 249, 112, 24, 241, 118, 212, 52, 22, 107, 202, 56, 105, 37, 68, 47, 169, 23, 142, 135], "sharesToWithdraw": [50000000000000]}`, + CreatedAt: time.Time{}, + UpdatedAt: time.Time{}, + DeletedAt: time.Time{}, + } + + model, err := NewStakerSharesModel(esm, grm, l, cfg) + assert.Nil(t, err) + + err = model.SetupStateForBlock(blockNumber) + assert.Nil(t, err) + + change, err := model.HandleStateChange(&log) + assert.Nil(t, err) + assert.NotNil(t, change) + + diffs := change.(*AccumulatedStateChanges) + assert.Equal(t, 1, len(diffs.Changes)) + + shareDiff := diffs.Changes[0] + assert.Equal(t, "-50000000000000", shareDiff.Shares) + assert.Equal(t, strings.ToLower("0x3c42cd72639e3e8d11ab8d0072cc13bd5d8aa83c"), shareDiff.Staker) + assert.Equal(t, "0xd523267698c81a372191136e477fdebfa33d9fb4", shareDiff.Strategy) + }) + + t.Run("Should capture Slashing withdrawals for multiple strategies", func(t *testing.T) { + esm := stateManager.NewEigenStateManager(l, grm) + blockNumber := uint64(200) + log := storage.TransactionLog{ + TransactionHash: "some hash", + TransactionIndex: big.NewInt(300).Uint64(), + BlockNumber: blockNumber, + Address: cfg.GetContractsMapForChain().DelegationManager, + Arguments: `[{"Name": "withdrawalRoot", "Type": "bytes32", "Value": ""}, {"Name": "withdrawal", "Type": "(address,address,address,uint256,uint32,address[],uint256[])", "Value": ""}]`, + EventName: "SlashingWithdrawalQueued", + LogIndex: big.NewInt(600).Uint64(), + OutputData: `{"withdrawal": {"nonce": 0, "scaledShares": [1000000000000000000, 2000000000000000000], "staker": "0x3c42cd72639e3e8d11ab8d0072cc13bd5d8aa83c", "startBlock": 1215690, "strategies": ["0xd523267698c81a372191136e477fdebfa33d9fb4", "0xe523267698c81a372191136e477fdebfa33d9fb5"], "withdrawer": "0x3c42cd72639e3e8d11ab8d0072cc13bd5d8aa83c", "delegatedTo": "0x2177dee1f66d6dbfbf517d9c4f316024c6a21aeb"}, "withdrawalRoot": [24, 23, 49, 137, 14, 63, 119, 12, 234, 225, 63, 35, 109, 249, 112, 24, 241, 118, 212, 52, 22, 107, 202, 56, 105, 37, 68, 47, 169, 23, 142, 135], "sharesToWithdraw": [50000000000000, 100000000000000]}`, + CreatedAt: time.Time{}, + UpdatedAt: time.Time{}, + DeletedAt: time.Time{}, + } + + model, err := NewStakerSharesModel(esm, grm, l, cfg) + assert.Nil(t, err) + + err = model.SetupStateForBlock(blockNumber) + assert.Nil(t, err) + + change, err := model.HandleStateChange(&log) + assert.Nil(t, err) + assert.NotNil(t, change) + + diffs := change.(*AccumulatedStateChanges) + assert.Equal(t, 2, len(diffs.Changes)) + + shareDiff := diffs.Changes[0] + assert.Equal(t, "-50000000000000", shareDiff.Shares) + assert.Equal(t, strings.ToLower("0x3c42cd72639e3e8d11ab8d0072cc13bd5d8aa83c"), shareDiff.Staker) + assert.Equal(t, "0xd523267698c81a372191136e477fdebfa33d9fb4", shareDiff.Strategy) + + shareDiff = diffs.Changes[1] + assert.Equal(t, "-100000000000000", shareDiff.Shares) + assert.Equal(t, strings.ToLower("0x3c42cd72639e3e8d11ab8d0072cc13bd5d8aa83c"), shareDiff.Staker) + assert.Equal(t, "0xe523267698c81a372191136e477fdebfa33d9fb5", shareDiff.Strategy) + }) + t.Cleanup(func() { postgres.TeardownTestDatabase(dbName, cfg, grm, l) })