Skip to content

Commit

Permalink
Calculate USD price per 1e18 of smallest token denomination with 18 d…
Browse files Browse the repository at this point in the history
…ecimal precision
  • Loading branch information
asoliman92 committed Aug 15, 2024
1 parent a64c23b commit f942eea
Show file tree
Hide file tree
Showing 3 changed files with 104 additions and 17 deletions.
76 changes: 59 additions & 17 deletions internal/reader/onchain_prices_reader.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,15 @@ import (
"math/big"

"github.com/smartcontractkit/chainlink-ccip/pkg/consts"
ocr2types "github.com/smartcontractkit/libocr/offchainreporting2plus/types"
"golang.org/x/sync/errgroup"

"github.com/smartcontractkit/chainlink-ccip/pluginconfig"

"github.com/smartcontractkit/libocr/offchainreporting2plus/types"
ocr2types "github.com/smartcontractkit/libocr/offchainreporting2plus/types"

commontyps "github.com/smartcontractkit/chainlink-common/pkg/types"

"github.com/smartcontractkit/chainlink-common/pkg/types/query/primitives"

"golang.org/x/sync/errgroup"
)

type TokenPrices interface {
Expand Down Expand Up @@ -63,21 +62,16 @@ func (pr *OnchainTokenPricesReader) GetTokenPricesUSD(
// Address: pr.PriceSources[token].AggregatorAddress,
// Name: consts.ContractNamePriceAggregator,
//}

latestRoundData := LatestRoundData{}
if err :=
pr.ContractReader.GetLatestValue(
ctx,
consts.ContractNamePriceAggregator,
consts.MethodNameGetLatestRoundData,
primitives.Finalized,
nil,
latestRoundData,
//boundContract,
); err != nil {
rawTokenPrice, err := pr.getRawTokenPrice(ctx, token)
if err != nil {
return fmt.Errorf("failed to get token price for %s: %w", token, err)
}
prices[idx] = latestRoundData.Answer
decimals, err := pr.getTokenDecimals(ctx, token)
if err != nil {
return fmt.Errorf("failed to get decimals for %s: %w", token, err)
}

prices[idx] = calculateUsdPer1e18TokenAmount(rawTokenPrice, *decimals)
return nil
})
}
Expand All @@ -95,5 +89,53 @@ func (pr *OnchainTokenPricesReader) GetTokenPricesUSD(
return prices, nil
}

func (pr *OnchainTokenPricesReader) getRawTokenPrice(ctx context.Context, token types.Account) (*big.Int, error) {
latestRoundData := LatestRoundData{}
if err :=
pr.ContractReader.GetLatestValue(
ctx,
consts.ContractNamePriceAggregator,
consts.MethodNameGetLatestRoundData,
primitives.Finalized,
nil,
latestRoundData,
//boundContract,
); err != nil {
return nil, fmt.Errorf("latestRoundData call failed for token %s: %w", token, err)
}

return latestRoundData.Answer, nil
}

func (pr *OnchainTokenPricesReader) getTokenDecimals(ctx context.Context, token types.Account) (*uint8, error) {
var decimals *uint8
if err :=
pr.ContractReader.GetLatestValue(
ctx,
consts.ContractNamePriceAggregator,
consts.MethodNameGetDecimals,
primitives.Finalized,
nil,
decimals,
//boundContract,
); err != nil {
return nil, fmt.Errorf("decimals call failed for token %s: %w", token, err)
}

return decimals, nil
}

// Input price is USD per full token, with 18 decimal precision
// Result price is USD per 1e18 of smallest token denomination, with 18 decimal precision
// Examples:
//
// 1 USDC = 1.00 USD per full token, each full token is 1e6 units -> 1 * 1e18 * 1e18 / 1e6 = 1e30
// 1 ETH = 2,000 USD per full token, each full token is 1e18 units -> 2000 * 1e18 * 1e18 / 1e18 = 2_000e18
// 1 LINK = 5.00 USD per full token, each full token is 1e18 units -> 5 * 1e18 * 1e18 / 1e18 = 5e18
func calculateUsdPer1e18TokenAmount(price *big.Int, decimals uint8) *big.Int {
tmp := big.NewInt(0).Mul(price, big.NewInt(1e18))
return tmp.Div(tmp, big.NewInt(0).Exp(big.NewInt(10), big.NewInt(int64(decimals)), nil))
}

// Ensure OnchainTokenPricesReader implements TokenPrices
var _ TokenPrices = (*OnchainTokenPricesReader)(nil)
44 changes: 44 additions & 0 deletions internal/reader/onchain_prices_reader_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ import (
"github.com/smartcontractkit/chainlink-ccip/internal/mocks"
"github.com/smartcontractkit/chainlink-ccip/pkg/consts"
"github.com/smartcontractkit/chainlink-ccip/pluginconfig"

ocr2types "github.com/smartcontractkit/libocr/offchainreporting2plus/types"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)
Expand Down Expand Up @@ -76,12 +78,54 @@ func TestOnchainTokenPricesReader_GetTokenPricesUSD(t *testing.T) {

}

func TestPriceService_calculateUsdPer1e18TokenAmount(t *testing.T) {
testCases := []struct {
name string
price *big.Int
decimal uint8
wantResult *big.Int
}{
{
name: "18-decimal token, $6.5 per token",
price: big.NewInt(65e17),
decimal: 18,
wantResult: big.NewInt(65e17),
},
{
name: "6-decimal token, $1 per token",
price: big.NewInt(1e18),
decimal: 6,
wantResult: new(big.Int).Mul(big.NewInt(1e18), big.NewInt(1e12)), // 1e30
},
{
name: "0-decimal token, $1 per token",
price: big.NewInt(1e18),
decimal: 0,
wantResult: new(big.Int).Mul(big.NewInt(1e18), big.NewInt(1e18)), // 1e36
},
{
name: "36-decimal token, $1 per token",
price: big.NewInt(1e18),
decimal: 36,
wantResult: big.NewInt(1),
},
}
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
got := calculateUsdPer1e18TokenAmount(tt.price, tt.decimal)
assert.Equal(t, tt.wantResult, got)
})
}
}

func createMockReader(
mockPrices map[ocr2types.Account]*big.Int,
errorAccounts []ocr2types.Account,
priceSources map[ocr2types.Account]pluginconfig.ArbitrumPriceSource,
) *mocks.ContractReaderMock {
reader := mocks.NewContractReaderMock()
println(errorAccounts)
println(priceSources)
// TODO: Create a list of bound contracts from priceSources and return the price given in mockPrices
for _, price := range mockPrices {
price := price
Expand Down
1 change: 1 addition & 0 deletions pkg/consts/consts.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ const (

// Aggregator methods
MethodNameGetLatestRoundData = "latestRoundData"
MethodNameGetDecimals = "decimals"

/*
// On EVM:
Expand Down

0 comments on commit f942eea

Please sign in to comment.