Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[CCIP-2958] Token price reader implementation #67

Merged
merged 22 commits into from
Aug 20, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
7a78a26
WIP new token price reader
asoliman92 Aug 14, 2024
b324085
Bind first token aggregator to price reader
asoliman92 Aug 14, 2024
a64c23b
Moving price reader binding to chainlink inprocess.go and removing fr…
asoliman92 Aug 14, 2024
f942eea
Calculate USD price per 1e18 of smallest token denomination with 18 d…
asoliman92 Aug 15, 2024
6829d88
Fix call to GetLatestValue
asoliman92 Aug 15, 2024
2b55c95
Merge branch 'ccip-develop' into price-reader
asoliman92 Aug 15, 2024
9c4952d
Add decimals to offchain config
asoliman92 Aug 16, 2024
dc73503
Use TokenDecimals in on chain reader
asoliman92 Aug 19, 2024
19851b0
Normalize raw token prices
asoliman92 Aug 19, 2024
8bcd098
Validate all tokens has decimals in offchain config
asoliman92 Aug 19, 2024
691c7a6
Add comments
asoliman92 Aug 19, 2024
cf816e6
Merge branch 'ccip-develop' into price-reader
asoliman92 Aug 19, 2024
c31764c
Add new on chain prices reader to commitocb factory
asoliman92 Aug 19, 2024
2fe0c01
Validate ArbitrumPriceSource in the offchain config
asoliman92 Aug 19, 2024
2bc1395
Add comments
asoliman92 Aug 19, 2024
4e88e66
Merge branch 'ccip-develop' into price-reader
asoliman92 Aug 19, 2024
6ef8ee4
Fix tests - failing because of race condition
asoliman92 Aug 19, 2024
2b33e54
Make the test work with one token as expected
asoliman92 Aug 20, 2024
952025a
Update pluginconfig/commit.go
asoliman92 Aug 20, 2024
1661bba
Update pluginconfig/commit.go
asoliman92 Aug 20, 2024
fad8a3a
review comments
asoliman92 Aug 20, 2024
0f52997
Merge branch 'ccip-develop' into price-reader
asoliman92 Aug 20, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Calculate USD price per 1e18 of smallest token denomination with 18 d…
…ecimal precision
  • Loading branch information
asoliman92 committed Aug 15, 2024
commit f942eead83905fce1de4dc4031d390345fc26401
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 {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm the token price isn't always to 18 decimals though, that depends on the feed.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is what the function is doing. It also shows in the tests, it's normalizing to 18 decimals depending on the decimals sent as a parameter.

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
Loading