Skip to content

Commit 752dbdf

Browse files
committed
node(governor): add some type safety; rename decimals to scalingFactor
- Change TokenConfigEntry.Decimals field from int64 to uint8 for more appropriate data type - Rename tokenEntry.decimals to scalingFactor with clarifying comment explaining it represents 10^decimals - Add comprehensive error checking in computeValue function to prevent nil pointer dereferences and division by zero - Add test cases to validate token prices are positive and decimals are within sensible bounds (0-24)
1 parent e6a1078 commit 752dbdf

File tree

3 files changed

+59
-18
lines changed

3 files changed

+59
-18
lines changed

node/pkg/governor/governor.go

Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ type (
5959
Addr string
6060
Symbol string
6161
CoinGeckoId string
62-
Decimals int64
62+
Decimals uint8
6363
Price float64
6464
}
6565

@@ -78,8 +78,9 @@ type (
7878

7979
// Payload of the map of the tokens being monitored
8080
tokenEntry struct {
81-
price *big.Float
82-
decimals *big.Int
81+
price *big.Float
82+
// scalingFactor is 10 to the power of the number of decimals of the [TokenConfigEntry].
83+
scalingFactor *big.Int
8384
symbol string
8485
coinGeckoId string
8586
token tokenKey
@@ -329,8 +330,9 @@ func (gov *ChainGovernor) initConfig() error {
329330
dec = 8
330331
}
331332

332-
decimalsFloat := big.NewFloat(math.Pow(10.0, float64(dec)))
333-
decimals, _ := decimalsFloat.Int(nil)
333+
// Calculate the scaling factor based on the number of decimals.
334+
scalingFactorFloat := big.NewFloat(math.Pow(10.0, float64(dec)))
335+
scalingFactor, _ := scalingFactorFloat.Int(nil)
334336

335337
// Some Solana tokens don't have the symbol set. In that case, use the chain and token address as the symbol.
336338
symbol := ct.Symbol
@@ -340,12 +342,12 @@ func (gov *ChainGovernor) initConfig() error {
340342

341343
key := tokenKey{chain: vaa.ChainID(ct.Chain), addr: addr}
342344
te := &tokenEntry{
343-
cfgPrice: cfgPrice,
344-
price: initialPrice,
345-
decimals: decimals,
346-
symbol: symbol,
347-
coinGeckoId: ct.CoinGeckoId,
348-
token: key,
345+
cfgPrice: cfgPrice,
346+
price: initialPrice,
347+
scalingFactor: scalingFactor,
348+
symbol: symbol,
349+
coinGeckoId: ct.CoinGeckoId,
350+
token: key,
349351
}
350352
te.updatePrice()
351353

@@ -366,8 +368,8 @@ func (gov *ChainGovernor) initConfig() error {
366368
zap.String("symbol", te.symbol),
367369
zap.String("coinGeckoId", te.coinGeckoId),
368370
zap.String("price", te.price.String()),
369-
zap.Int64("decimals", dec),
370-
zap.Int64("origDecimals", ct.Decimals),
371+
zap.Uint8("decimals", dec),
372+
zap.Uint8("origDecimals", ct.Decimals),
371373
)
372374
}
373375
}
@@ -946,15 +948,31 @@ func (gov *ChainGovernor) CheckPendingForTime(now time.Time) ([]*common.MessageP
946948
return msgsToPublish, nil
947949
}
948950

951+
// computeValue computes the USD value of a transfer.
949952
func computeValue(amount *big.Int, token *tokenEntry) (uint64, error) {
953+
954+
if token.scalingFactor == nil {
955+
return 0, fmt.Errorf("scalingFactor is nil")
956+
}
957+
958+
if token.price == nil {
959+
return 0, fmt.Errorf("price is nil")
960+
}
961+
962+
// Prevent division by zero panic.
963+
// Shouldn't occur in practice as scalingFactor is always a power of 10.
964+
if token.scalingFactor.Cmp(big.NewInt(0)) == 0 {
965+
return 0, fmt.Errorf("scalingFactor is zero")
966+
}
967+
950968
amountFloat := new(big.Float)
951969
amountFloat = amountFloat.SetInt(amount)
952970

953971
valueFloat := new(big.Float)
954972
valueFloat = valueFloat.Mul(amountFloat, token.price)
955973

956974
valueBigInt, _ := valueFloat.Int(nil)
957-
valueBigInt = valueBigInt.Div(valueBigInt, token.decimals)
975+
valueBigInt = valueBigInt.Div(valueBigInt, token.scalingFactor)
958976

959977
if !valueBigInt.IsUint64() {
960978
return 0, fmt.Errorf("value is too large to fit in uint64")

node/pkg/governor/governor_test.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ func (gov *ChainGovernor) initConfigForTest(
4343
decimals, _ := decimalsFloat.Int(nil)
4444
key := tokenKey{chain: tokenChainID, addr: tokenAddr}
4545

46-
gov.tokens[key] = &tokenEntry{price: price, decimals: decimals, symbol: tokenSymbol, token: key}
46+
gov.tokens[key] = &tokenEntry{price: price, scalingFactor: decimals, symbol: tokenSymbol, token: key}
4747
}
4848

4949
func (gov *ChainGovernor) setDayLengthInMinutes(minimum int) {
@@ -89,17 +89,19 @@ func (gov *ChainGovernor) setTokenForTesting(
8989
gov.mutex.Lock()
9090
defer gov.mutex.Unlock()
9191

92+
const Decimals = 8
93+
9294
tokenAddr, err := vaa.StringToAddress(tokenAddrStr)
9395
if err != nil {
9496
return err
9597
}
9698

9799
bigPrice := big.NewFloat(price)
98-
decimalsFloat := big.NewFloat(math.Pow(10.0, float64(8)))
99-
decimals, _ := decimalsFloat.Int(nil)
100+
scalingFactorFloat := big.NewFloat(math.Pow(10.0, float64(Decimals)))
101+
scalingFactor, _ := scalingFactorFloat.Int(nil)
100102

101103
key := tokenKey{chain: tokenChainID, addr: tokenAddr}
102-
te := &tokenEntry{cfgPrice: bigPrice, price: bigPrice, decimals: decimals, symbol: symbol, coinGeckoId: symbol, token: key, flowCancels: flowCancels}
104+
te := &tokenEntry{cfgPrice: bigPrice, price: bigPrice, scalingFactor: scalingFactor, symbol: symbol, coinGeckoId: symbol, token: key, flowCancels: flowCancels}
103105
gov.tokens[key] = te
104106
cge, cgExists := gov.tokensByCoinGeckoId[te.coinGeckoId]
105107
if !cgExists {

node/pkg/governor/mainnet_tokens_test.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package governor
22

33
import (
44
"fmt"
5+
"slices"
56
"strings"
67
"testing"
78

@@ -33,6 +34,26 @@ func TestTokenListAddressSize(t *testing.T) {
3334
}
3435
}
3536

37+
func TestTokenPositivePrices(t *testing.T) {
38+
tokenConfigEntries := TokenList()
39+
40+
for tokenConfigEntry := range slices.Values(tokenConfigEntries) {
41+
assert.Greater(t, tokenConfigEntry.Price, float64(0), "Token price must be greater than zero")
42+
}
43+
}
44+
45+
func TestTokenSensibleDecimals(t *testing.T) {
46+
tokenConfigEntries := TokenList()
47+
// This is the global maximum number of decimals among the tokens we have configured. (due to NEAR)
48+
// A larger number may be fine but it's unusual, so it's worth flagging.
49+
const maxDecimals = 24
50+
51+
for tokenConfigEntry := range slices.Values(tokenConfigEntries) {
52+
assert.GreaterOrEqual(t, tokenConfigEntry.Decimals, uint8(0), "Token decimals must be greater than or equal to zero")
53+
assert.LessOrEqual(t, tokenConfigEntry.Decimals, uint8(maxDecimals), fmt.Sprintf("Token decimals must be less than or equal to %d but got %d. details: %v", maxDecimals, tokenConfigEntry.Decimals, tokenConfigEntry))
54+
}
55+
}
56+
3657
// Flag a situation where a Governed chain does not have any governed assets. Often times when adding a mainnet chain,
3758
// a list of tokens will be added so that they can be governed. (These tokens are sourced by CoinGecko or manually
3859
// populated.) While this is not a hard requirement, it may represent that a developer has forgotten to take the step

0 commit comments

Comments
 (0)