Skip to content

Commit 15d02ed

Browse files
author
corey
committed
fix
1 parent 45ff9f5 commit 15d02ed

File tree

12 files changed

+187
-51
lines changed

12 files changed

+187
-51
lines changed
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Build Geth in a stock Go builder container
1+
# Build token-price-oracle in a stock Go builder container
22
FROM ghcr.io/morph-l2/go-ubuntu-builder:go-1.24-ubuntu as builder
33

44
COPY . /morph
@@ -7,11 +7,11 @@ WORKDIR /morph/token-price-oracle
77

88
RUN make build
99

10-
# Pull Geth into a second stage deploy alpine container
10+
# Copy binary into a lightweight runtime container
1111
FROM ghcr.io/morph-l2/go-ubuntu-builder:go-1.24-ubuntu
1212

1313
RUN apt-get -qq update \
1414
&& apt-get -qq install -y --no-install-recommends ca-certificates
15-
COPY --from=builder /morph/token-price-oracle/token-price-oracle /usr/local/bin/
15+
COPY --from=builder /morph/token-price-oracle/build/bin/token-price-oracle /usr/local/bin/
1616

1717
CMD ["token-price-oracle"]

token-price-oracle/Dockerfile

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,18 +25,18 @@ RUN apk --no-cache add ca-certificates
2525
WORKDIR /root/
2626

2727
# Copy binary from build stage
28-
COPY --from=builder /app/bin/gas-price-oracle .
28+
COPY --from=builder /app/build/bin/token-price-oracle .
2929

3030
# Create log directory
31-
RUN mkdir -p /data/logs/morph-gas-oracle
31+
RUN mkdir -p /data/logs/token-price-oracle
3232

3333
# Expose metrics port
3434
EXPOSE 6060
3535

3636
# Health check endpoint
3737
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
38-
CMD wget --no-verbose --tries=1 --spider http://localhost:6060/health || exit 1
38+
CMD wget --no-verbose --tries=1 --spider http://localhost:6060/metrics || exit 1
3939

4040
# Run service
41-
ENTRYPOINT ["./gas-price-oracle"]
41+
ENTRYPOINT ["./token-price-oracle"]
4242

token-price-oracle/Makefile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,15 @@ VERSION := v0.1.0
66

77
LDFLAGSSTRING +=-X main.GitCommit=$(GITCOMMIT)
88
LDFLAGSSTRING +=-X main.GitDate=$(GITDATE)
9-
LDFLAGSSTRING +=-X main.Version=$(VERSION)
9+
LDFLAGSSTRING +=-X main.GitVersion=$(VERSION)
1010
LDFLAGS := -ldflags "$(LDFLAGSSTRING)"
1111

1212
build:
1313
if [ ! -d build/bin ]; then mkdir -p build/bin; fi
1414
go mod download
1515
env GO111MODULE=on CGO_ENABLED=1 CGO_LDFLAGS="-ldl" go build -o build/bin/token-price-oracle -v $(LDFLAGS) ./cmd
1616
run: build
17-
.build/bin/token-price-oracle
17+
./build/bin/token-price-oracle
1818

1919
test:
2020
go test -v ./...

token-price-oracle/client/bitget_sdk.go

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"math/big"
99
"net/http"
1010
"strconv"
11+
"sync"
1112
"time"
1213

1314
"github.com/morph-l2/go-ethereum/log"
@@ -18,10 +19,12 @@ const (
1819
)
1920

2021
// BitgetSDKPriceFeed uses Bitget REST API to fetch prices
22+
// This type is safe for concurrent use by multiple goroutines
2123
type BitgetSDKPriceFeed struct {
2224
httpClient *http.Client
23-
tokenMap map[uint16]string
24-
ethPrice *big.Float
25+
mu sync.RWMutex // protects tokenMap and ethPrice
26+
tokenMap map[uint16]string // guarded by mu
27+
ethPrice *big.Float // guarded by mu
2528
log log.Logger
2629
baseURL string
2730
}
@@ -61,7 +64,11 @@ func NewBitgetSDKPriceFeed(tokenMap map[uint16]string, baseURL string) *BitgetSD
6164
// GetTokenPrice returns token price in USD
6265
// Note: Caller should ensure ETH price is updated via GetBatchTokenPrices for batch operations
6366
func (b *BitgetSDKPriceFeed) GetTokenPrice(ctx context.Context, tokenID uint16) (*TokenPrice, error) {
67+
b.mu.RLock()
6468
symbol, exists := b.tokenMap[tokenID]
69+
ethPrice := new(big.Float).Copy(b.ethPrice)
70+
b.mu.RUnlock()
71+
6572
if !exists {
6673
return nil, fmt.Errorf("token ID %d not mapped to trading pair", tokenID)
6774
}
@@ -73,7 +80,7 @@ func (b *BitgetSDKPriceFeed) GetTokenPrice(ctx context.Context, tokenID uint16)
7380
}
7481

7582
// Use cached ETH price (should be updated by GetBatchTokenPrices)
76-
if b.ethPrice.Cmp(big.NewFloat(0)) == 0 {
83+
if ethPrice.Cmp(big.NewFloat(0)) == 0 {
7784
return nil, fmt.Errorf("ETH price not initialized, please call GetBatchTokenPrices first")
7885
}
7986

@@ -82,18 +89,19 @@ func (b *BitgetSDKPriceFeed) GetTokenPrice(ctx context.Context, tokenID uint16)
8289
"token_id", tokenID,
8390
"symbol", symbol,
8491
"token_price_usd", tokenPrice.String(),
85-
"eth_price_usd", b.ethPrice.String())
92+
"eth_price_usd", ethPrice.String())
8693

8794
return &TokenPrice{
8895
TokenID: tokenID,
8996
Symbol: symbol,
9097
TokenPriceUSD: tokenPrice,
91-
EthPriceUSD: b.ethPrice,
98+
EthPriceUSD: ethPrice,
9299
}, nil
93100
}
94101

95102
// GetBatchTokenPrices returns batch token prices in USD
96103
func (b *BitgetSDKPriceFeed) GetBatchTokenPrices(ctx context.Context, tokenIDs []uint16) (map[uint16]*TokenPrice, error) {
104+
// Update ETH price first (this will acquire write lock)
97105
if err := b.updateETHPrice(ctx); err != nil {
98106
return nil, fmt.Errorf("failed to update ETH price: %w", err)
99107
}
@@ -121,7 +129,10 @@ func (b *BitgetSDKPriceFeed) updateETHPrice(ctx context.Context) error {
121129
return fmt.Errorf("failed to fetch ETH price: %w", err)
122130
}
123131

132+
b.mu.Lock()
124133
b.ethPrice = price
134+
b.mu.Unlock()
135+
125136
b.log.Info("Fetched ETH price from Bitget",
126137
"source", "bitget",
127138
"symbol", "ETHUSDT",
@@ -227,16 +238,27 @@ func (b *BitgetSDKPriceFeed) fetchPriceOnce(ctx context.Context, symbol string)
227238
}
228239

229240
// UpdateTokenMap updates token mapping
241+
// This method is safe to call concurrently with other methods
242+
// The input map is copied to prevent external modifications
230243
func (b *BitgetSDKPriceFeed) UpdateTokenMap(tokenMap map[uint16]string) {
231-
b.tokenMap = tokenMap
232-
b.log.Info("Updated token map", "token_map", tokenMap)
244+
b.mu.Lock()
245+
// Create a defensive copy to prevent external modifications
246+
copied := make(map[uint16]string, len(tokenMap))
247+
for k, v := range tokenMap {
248+
copied[k] = v
249+
}
250+
b.tokenMap = copied
251+
b.mu.Unlock()
252+
b.log.Info("Updated token map", "token_map", copied)
233253
}
234254

235255
// GetSupportedTokens returns list of supported token IDs
236256
func (b *BitgetSDKPriceFeed) GetSupportedTokens() []uint16 {
257+
b.mu.RLock()
237258
tokenIDs := make([]uint16, 0, len(b.tokenMap))
238259
for tokenID := range b.tokenMap {
239260
tokenIDs = append(tokenIDs, tokenID)
240261
}
262+
b.mu.RUnlock()
241263
return tokenIDs
242264
}

token-price-oracle/client/l2_client.go

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ package client
22

33
import (
44
"context"
5-
"errors"
5+
"fmt"
66
"math/big"
77

88
"github.com/morph-l2/go-ethereum/accounts/abi/bind"
@@ -22,13 +22,20 @@ type L2Client struct {
2222
func NewL2Client(rpcURL string, privateKey string) (*L2Client, error) {
2323
client, err := ethclient.Dial(rpcURL)
2424
if err != nil {
25-
return nil, err
25+
return nil, fmt.Errorf("failed to dial L2 RPC: %w", err)
2626
}
2727

28+
// Ensure client is closed if any subsequent step fails
29+
defer func() {
30+
if err != nil {
31+
client.Close()
32+
}
33+
}()
34+
2835
// Get chain ID
2936
chainID, err := client.ChainID(context.Background())
3037
if err != nil {
31-
return nil, err
38+
return nil, fmt.Errorf("failed to get chain ID: %w", err)
3239
}
3340

3441
// Parse private key (remove 0x prefix if present)
@@ -38,13 +45,13 @@ func NewL2Client(rpcURL string, privateKey string) (*L2Client, error) {
3845
}
3946
key, err := crypto.HexToECDSA(privateKeyHex)
4047
if err != nil {
41-
return nil, errors.New("invalid hex character 'x' in private key")
48+
return nil, fmt.Errorf("failed to parse private key: %w", err)
4249
}
4350

4451
// Create transaction options
4552
opts, err := bind.NewKeyedTransactorWithChainID(key, chainID)
4653
if err != nil {
47-
return nil, err
54+
return nil, fmt.Errorf("failed to create transactor: %w", err)
4855
}
4956

5057
return &L2Client{
@@ -64,9 +71,22 @@ func (c *L2Client) GetClient() *ethclient.Client {
6471
return c.client
6572
}
6673

67-
// GetOpts returns transaction options
74+
// GetOpts returns a copy of transaction options
75+
// Returns a new instance to prevent concurrent modification
6876
func (c *L2Client) GetOpts() *bind.TransactOpts {
69-
return c.opts
77+
// Return a copy to prevent shared state issues
78+
return &bind.TransactOpts{
79+
From: c.opts.From,
80+
Nonce: c.opts.Nonce,
81+
Signer: c.opts.Signer,
82+
Value: c.opts.Value,
83+
GasPrice: c.opts.GasPrice,
84+
GasFeeCap: c.opts.GasFeeCap,
85+
GasTipCap: c.opts.GasTipCap,
86+
GasLimit: c.opts.GasLimit,
87+
Context: c.opts.Context,
88+
NoSend: c.opts.NoSend,
89+
}
7090
}
7191

7292
// GetBalance returns account balance

token-price-oracle/client/price_feed.go

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package client
22

33
import (
44
"context"
5+
"fmt"
56
"math/big"
67

78
"github.com/morph-l2/go-ethereum/log"
@@ -54,6 +55,16 @@ func (f *FallbackPriceFeed) GetTokenPrice(ctx context.Context, tokenID uint16) (
5455

5556
price, err := feed.GetTokenPrice(ctx, tokenID)
5657
if err == nil {
58+
// Validate returned price to prevent nil pointer panics
59+
if price == nil || price.TokenPriceUSD == nil || price.EthPriceUSD == nil {
60+
f.log.Warn("Feed returned nil price or components, treating as failure",
61+
"token_id", tokenID,
62+
"feed", feedName,
63+
"priority", i)
64+
lastErr = fmt.Errorf("feed %s returned incomplete price for token %d", feedName, tokenID)
65+
continue
66+
}
67+
5768
f.log.Info("Successfully fetched price from feed",
5869
"source", feedName,
5970
"token_id", tokenID,
@@ -87,8 +98,27 @@ func (f *FallbackPriceFeed) GetBatchTokenPrices(ctx context.Context, tokenIDs []
8798

8899
prices, err := feed.GetBatchTokenPrices(ctx, tokenIDs)
89100
if err == nil {
101+
// Validate all returned prices to prevent nil pointer panics
102+
hasInvalidPrice := false
103+
for tokenID, price := range prices {
104+
if price == nil || price.TokenPriceUSD == nil || price.EthPriceUSD == nil {
105+
f.log.Warn("Feed returned nil price or components for token, treating as failure",
106+
"token_id", tokenID,
107+
"feed", feedName,
108+
"priority", i)
109+
hasInvalidPrice = true
110+
break
111+
}
112+
}
113+
114+
if hasInvalidPrice {
115+
lastErr = fmt.Errorf("feed %s returned incomplete prices", feedName)
116+
continue
117+
}
118+
90119
f.log.Info("Successfully fetched batch prices from feed",
91-
"token_count", len(tokenIDs),
120+
"token_count", len(prices),
121+
"requested_count", len(tokenIDs),
92122
"feed", feedName,
93123
"priority", i)
94124
return prices, nil

token-price-oracle/config/config.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,11 @@ func LoadConfig(ctx *cli.Context) (*Config, error) {
116116
}
117117

118118
cfg.PriceThreshold = ctx.Uint64(flags.PriceThresholdFlag.Name)
119+
120+
// Validate price threshold is reasonable (percentage should be 0-100)
121+
if cfg.PriceThreshold > 100 {
122+
return nil, fmt.Errorf("price threshold %d is too large (should be 0-100 for percentage)", cfg.PriceThreshold)
123+
}
119124

120125
// Parse and validate price feed priority list
121126
priorityStr := ctx.String(flags.PriceFeedPriorityFlag.Name)

token-price-oracle/docker-compose.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ services:
1616

1717
# Price update configuration
1818
TOKEN_PRICE_ORACLE_PRICE_UPDATE_INTERVAL: ${TOKEN_PRICE_ORACLE_PRICE_UPDATE_INTERVAL:-30s}
19-
TOKEN_PRICE_ORACLE_PRICE_THRESHOLD: ${TOKEN_PRICE_ORACLE_PRICE_THRESHOLD:-5}
19+
TOKEN_PRICE_ORACLE_PRICE_THRESHOLD: ${TOKEN_PRICE_ORACLE_PRICE_THRESHOLD:-5} # percentage (%)
2020

2121
# Price feed configuration
2222
TOKEN_PRICE_ORACLE_PRICE_FEED_PRIORITY: ${TOKEN_PRICE_ORACLE_PRICE_FEED_PRIORITY:-bitget}

token-price-oracle/env.example

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ TOKEN_PRICE_ORACLE_PRIVATE_KEY=ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efca
1212

1313
# Price update configuration
1414
TOKEN_PRICE_ORACLE_PRICE_UPDATE_INTERVAL=30s
15-
TOKEN_PRICE_ORACLE_PRICE_THRESHOLD=5
15+
TOKEN_PRICE_ORACLE_PRICE_THRESHOLD=5 # percentage (%), e.g. 5 means 5% price change triggers update
1616

1717
# Price feed priority (comma-separated: bitget,binance)
1818
TOKEN_PRICE_ORACLE_PRICE_FEED_PRIORITY=bitget
@@ -34,7 +34,6 @@ TOKEN_PRICE_ORACLE_TOKEN_IDS=1,2
3434
TOKEN_PRICE_ORACLE_METRICS_SERVER_ENABLE=true
3535
TOKEN_PRICE_ORACLE_METRICS_HOSTNAME=0.0.0.0
3636
TOKEN_PRICE_ORACLE_METRICS_PORT=6060
37-
METRICS_PORT=6060
3837

3938
# Logging
4039
TOKEN_PRICE_ORACLE_LOG_LEVEL=info

token-price-oracle/flags/flags.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import (
66
"github.com/urfave/cli"
77
)
88

9-
const envVarPrefix = "GAS_ORACLE_"
9+
const envVarPrefix = "TOKEN_PRICE_ORACLE_"
1010

1111
func prefixEnvVar(name string) string {
1212
return envVarPrefix + name
@@ -19,7 +19,6 @@ var (
1919
Usage: "HTTP provider URL for L2",
2020
Required: true,
2121
EnvVar: prefixEnvVar("L2_ETH_RPC"),
22-
Value: "http://127.0.0.1:8545",
2322
}
2423

2524
PrivateKeyFlag = cli.StringFlag{

0 commit comments

Comments
 (0)