Skip to content

Commit

Permalink
Fix a Chainlink race using a cache with goroutines
Browse files Browse the repository at this point in the history
  • Loading branch information
user committed Dec 14, 2023
1 parent d980c4b commit 47d4895
Show file tree
Hide file tree
Showing 3 changed files with 110 additions and 42 deletions.
64 changes: 22 additions & 42 deletions common/ethereum/chainlink/chainlink.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,16 @@ import (
"math/big"

ethAbi "github.com/ethereum/go-ethereum/accounts/abi"
"github.com/fluidity-money/fluidity-app/common/ethereum"
"github.com/fluidity-money/fluidity-app/lib/log"

ethCommon "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/fluidity-money/fluidity-app/common/ethereum"
)

// Context to use for logging
const Context = "CHAINLINK"

const chainlinkPriceFeedAbiString = `[
{
"inputs": [],
Expand Down Expand Up @@ -65,53 +70,28 @@ const chainlinkPriceFeedAbiString = `[

var priceFeedAbi ethAbi.ABI

var decimalsCache = make(map[ethCommon.Address]*big.Rat)

func getFeedDecimals(client *ethclient.Client, priceFeedAddress ethCommon.Address) (*big.Rat, error) {
decimals, exists := decimalsCache[priceFeedAddress]

if exists {
return decimals, nil
}

decimalsRes, err := ethereum.StaticCall(
client,
priceFeedAddress,
priceFeedAbi,
"decimals",
)

if err != nil {
return nil, err
}

decimals_, err := ethereum.CoerceBoundContractResultsToUint8(decimalsRes)
// GetPrice using a Chainlink feed, caching the results in an internal
// server to make it thread safe
func GetPrice(client *ethclient.Client, priceFeedAddress ethCommon.Address) (*big.Rat, error) {
log.Debug(func(k *log.Log) {
k.Context = Context

if err != nil {
return nil, fmt.Errorf(
"Failed to read decimals result! %w",
err,
k.Format(
"Using the Chainlink decimals cache to look up decimals for price feed %v",
priceFeedAddress,
)
}

ten := big.NewRat(10, 1)
})

decimals = ethereum.BigPow(ten, int(decimals_))
decimalsCache[priceFeedAddress] = decimals

return decimals, nil
}
resp := make(chan *big.Rat)

func GetPrice(client *ethclient.Client, priceFeedAddress ethCommon.Address) (*big.Rat, error) {
decimals, err := getFeedDecimals(client, priceFeedAddress)

if err != nil {
return nil, fmt.Errorf(
"Failed to get feed decimals! %w",
err,
)
decimalsServer <- request{
client: client,
priceFeedAddress: priceFeedAddress,
resp: resp,
}

decimals := <-resp

priceRes, err := ethereum.StaticCall(
client,
priceFeedAddress,
Expand Down
2 changes: 2 additions & 0 deletions common/ethereum/chainlink/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,6 @@ func init() {
if priceFeedAbi, err = ethAbi.JSON(priceFeedReader); err != nil {
panic(err)
}

go startDecimalsServer()
}
86 changes: 86 additions & 0 deletions common/ethereum/chainlink/server.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// Copyright 2022 Fluidity Money. All rights reserved. Use of this
// source code is governed by a GPL-style license that can be found in the
// LICENSE.md file.

package chainlink

import (
"math/big"

ethCommon "github.com/ethereum/go-ethereum/common"
"github.com/fluidity-money/fluidity-app/common/ethereum"
"github.com/fluidity-money/fluidity-app/lib/log"

"github.com/ethereum/go-ethereum/ethclient"
)

type request struct {
client *ethclient.Client
priceFeedAddress ethCommon.Address
resp chan *big.Rat
}

// decimalsServer to respond to requests with cached (or not, looked up)
// responses
var decimalsServer = make(chan request)

func startDecimalsServer() {
decimalsCache := make(map[ethCommon.Address]*big.Rat)

for request := range decimalsServer {
var (
client = request.client
priceFeedAddress = request.priceFeedAddress
respChan = request.resp
)

decimals, exists := decimalsCache[priceFeedAddress]

if exists {
respChan <- decimals
continue
}

decimalsRes, err := ethereum.StaticCall(
client,
priceFeedAddress,
priceFeedAbi,
"decimals",
)

if err != nil {
log.Fatal(func(k *log.Log) {
k.Context = Context

k.Format(
"Failed to static call feed address %v",
priceFeedAddress,
)

k.Payload = err
})
}

decimals_, err := ethereum.CoerceBoundContractResultsToUint8(decimalsRes)

if err != nil {
log.Fatal(func(k *log.Log) {
k.Context = Context

k.Format(
"Failed to read decimals result for price feed address %v",
priceFeedAddress,
)

k.Payload = err
})
}

ten := big.NewRat(10, 1)

decimals = ethereum.BigPow(ten, int(decimals_))
decimalsCache[priceFeedAddress] = decimals

respChan <- decimals
}
}

0 comments on commit 47d4895

Please sign in to comment.