Skip to content

Commit

Permalink
feat: Check profitability before requesting batches (mvp) (#216)
Browse files Browse the repository at this point in the history
* feat: check profitability before requesting batches (mvp)

* fix unit tests

* fix lint

* fix e2e

* cl++

* lint++

* Apply suggestions from code review

Co-authored-by: Aleksandr Bezobchuk <[email protected]>

* fix suggestions

Co-authored-by: Aleksandr Bezobchuk <[email protected]>
Co-authored-by: Aleksandr Bezobchuk <[email protected]>
  • Loading branch information
3 people authored Feb 28, 2022
1 parent a345acb commit 996e522
Show file tree
Hide file tree
Showing 10 changed files with 119 additions and 7 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ Ref: https://keepachangelog.com/en/1.0.0/

## [Unreleased]

### Features

[#216](https://github.com/umee-network/peggo/pull/216) Add profitability check on the batch requester loop.

### Bug Fixes

- [#217](https://github.com/umee-network/peggo/pull/217) Add validation to user input Ethereum addresses.
Expand Down
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ RUN make install

# Fetch umeed binary
FROM golang:1.17-alpine AS umeed-builder
ARG UMEE_VERSION=v0.7.1
ARG UMEE_VERSION=v1.0.3
ENV PACKAGES curl make git libc-dev bash gcc linux-headers eudev-dev
RUN apk add --no-cache $PACKAGES
WORKDIR /downloads/
RUN git clone https://github.com/umee-network/umee.git
RUN cd umee && git checkout ${UMEE_VERSION} && make build && cp ./build/umeed /usr/local/bin/
RUN cd umee && git checkout ${UMEE_VERSION} && CGO_ENABLED=0 make build && cp ./build/umeed /usr/local/bin/

# Add to a distroless container
FROM gcr.io/distroless/cc:$IMG_TAG
Expand Down
1 change: 1 addition & 0 deletions cmd/peggo/orchestrator.go
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,7 @@ func getOrchestratorCmd() *cobra.Command {
batchRequesterLoopDuration,
konfig.Int64(flagEthBlocksPerLoop),
konfig.Int64(flagBridgeStartHeight),
coingeckoFeed,
)

ctx, cancel = context.WithCancel(context.Background())
Expand Down
3 changes: 2 additions & 1 deletion orchestrator/coingecko/coingecko.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
const (
maxRespTime = 15 * time.Second
maxRespHeadersTime = 15 * time.Second
EthereumCoinID = "ethereum"
)

var zeroPrice = float64(0)
Expand Down Expand Up @@ -98,7 +99,7 @@ func (cp *PriceFeed) QueryTokenUSDPrice(erc20Contract ethcmn.Address) (float64,
return cp.QueryUSDPriceByCoinID(coinID)
}

u, err := url.ParseRequestURI(urlJoin(cp.config.BaseURL, "simple", "token_price", "ethereum"))
u, err := url.ParseRequestURI(urlJoin(cp.config.BaseURL, "simple", "token_price", EthereumCoinID))
if err != nil {
cp.logger.Fatal().Err(err).Msg("failed to parse URL")
}
Expand Down
5 changes: 5 additions & 0 deletions orchestrator/eth_event_watcher_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ func TestCheckForEvents(t *testing.T) {
time.Second,
100,
0,
nil,
)

currentBlock, err := orch.CheckForEvents(context.Background(), 1, 5)
Expand Down Expand Up @@ -244,6 +245,7 @@ func TestCheckForEvents(t *testing.T) {
time.Second,
100,
0,
nil,
)

currentBlock, err := orch.CheckForEvents(context.Background(), 1, 5)
Expand Down Expand Up @@ -351,6 +353,7 @@ func TestCheckForEvents(t *testing.T) {
time.Second,
100,
0,
nil,
)

currentBlock, err := orch.CheckForEvents(context.Background(), 1, 5)
Expand Down Expand Up @@ -472,6 +475,7 @@ func TestCheckForEvents(t *testing.T) {
time.Second,
100,
0,
nil,
)

currentBlock, err := orch.CheckForEvents(context.Background(), 1, 5)
Expand Down Expand Up @@ -607,6 +611,7 @@ func TestCheckForEvents(t *testing.T) {
time.Second,
100,
0,
nil,
)

currentBlock, err := orch.CheckForEvents(context.Background(), 1, 5)
Expand Down
95 changes: 92 additions & 3 deletions orchestrator/main_loops.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,16 @@ package orchestrator
import (
"context"
"errors"
"fmt"
"math/big"
"time"

"github.com/avast/retry-go"
ethcmn "github.com/ethereum/go-ethereum/common"
"github.com/shopspring/decimal"
"github.com/umee-network/Gravity-Bridge/module/x/gravity/types"

"github.com/umee-network/peggo/orchestrator/coingecko"
"github.com/umee-network/peggo/orchestrator/loops"
)

Expand All @@ -26,6 +30,28 @@ const (
ethSignerLoopMultiplier = 3
)

// estimatedGasCosts has a list of gas costs for batches from 1 to 100 txs.
// They are used to estimate how much it will cost to relay a batch before
// it is "built" and signed.
// At the moment these values come from Umee's testnet data. It's in the roamap
// to add a dynamic registry in which gas costs are updated and divided by
// type of token (not all ERC20 are created equal, some may do some extra checks
// in transfers).
var estimatedGasCosts = []int64{
575563, 582863, 591565, 600967, 611968, 621532, 630328, 642386, 653063,
661581, 668183, 678635, 685289, 696851, 704866, 708887, 712721, 721445,
727461, 734690, 742043, 752750, 760223, 767272, 769101, 773423, 784019,
798268, 802351, 806362, 807763, 814683, 828969, 831213, 843207, 847569,
870002, 873950, 875285, 877254, 882126, 887008, 911510, 911901, 918882,
919109, 920685, 927237, 933757, 935638, 936261, 947621, 948716, 965708,
970508, 976337, 995011, 998407, 999148, 1016724, 1024643, 1035313,
1044177, 1046768, 1053295, 1053903, 1059293, 1073982, 1078022, 1078123,
1082061, 1084901, 1094332, 1103762, 1108249, 1114666, 1126675, 1136556,
1146072, 1154187, 1157889, 1159855, 1171010, 1172318, 1173955, 1181863,
1188274, 1191781, 1194480, 1209858, 1226168, 1227017, 1228247, 1234944,
1238819, 1244511, 1256137, 1258859, 1261745, 1261934,
}

// Start combines the all major roles required to make
// up the Orchestrator, all of these are async loops.
func (p *gravityOrchestrator) Start(ctx context.Context) error {
Expand Down Expand Up @@ -282,6 +308,11 @@ func (p *gravityOrchestrator) BatchRequesterLoop(ctx context.Context) (err error
// - broadcast Request batch
var pg loops.ParanoidGroup

usdEthPriceDec := decimal.NewFromFloat(0.0)
gasPrice := big.NewInt(0)
tokensPrices := make(map[string]decimal.Decimal)
tokensDecimals := make(map[string]uint8)

pg.Go(func() error {
var unbatchedTokensWithFees []types.BatchFees

Expand All @@ -294,6 +325,39 @@ func (p *gravityOrchestrator) BatchRequesterLoop(ctx context.Context) (err error

unbatchedTokensWithFees = batchFeesResp.GetBatchFees()

if p.relayer.GetProfitMultiplier() > 0.0 {
gasPrice, err = p.ethProvider.SuggestGasPrice(context.Background())
if err != nil {
return fmt.Errorf("failed to get Ethereum gas estimate: %w", err)
}

usdEthPrice, err := p.priceFeeder.QueryUSDPriceByCoinID(coingecko.EthereumCoinID)
if err != nil {
return err
}

usdEthPriceDec = decimal.NewFromFloat(usdEthPrice)

for _, token := range unbatchedTokensWithFees {
if _, ok := tokensPrices[token.Token]; !ok {
price, err := p.priceFeeder.QueryTokenUSDPrice(ethcmn.HexToAddress(token.Token))
if err != nil {
return err
}

tokensPrices[token.Token] = decimal.NewFromFloat(price)

tokensDecimals[token.Token], err = p.gravityContract.GetERC20Decimals(
ctx,
ethcmn.HexToAddress(token.Token),
p.gravityContract.FromAddress(),
)
if err != nil {
return err
}
}
}
}
return
}, retry.Context(ctx), retry.OnRetry(func(n uint, err error) {
logger.Err(err).Uint("retry", n).Msg("failed to get UnbatchedTokensWithFees; retrying...")
Expand All @@ -314,10 +378,35 @@ func (p *gravityOrchestrator) BatchRequesterLoop(ctx context.Context) (err error
return nil
}

logger.Info().Str("token_contract", tokenAddr.String()).Str("denom", denom).Msg("sending batch request")
shouldRequestBatch := true

if p.relayer.GetProfitMultiplier() > 0.0 {
// First we get the cost of the transaction in USD
totalETHcost := big.NewInt(0).Mul(gasPrice, big.NewInt(estimatedGasCosts[unbatchedToken.TxCount-1]))
// Ethereum decimals are 18 and that's a constant.
gasCostInUSDDec := decimal.NewFromBigInt(totalETHcost, -18).Mul(usdEthPriceDec)
// Decimals (uint8) can be safely casted into int32 because the max uint8 is 255 and the max int32 is 2147483647.
totalFeeInUSDDec := decimal.NewFromBigInt(
unbatchedToken.TotalFees.BigInt(),
-int32(tokensDecimals[unbatchedToken.Token]),
).Mul(tokensPrices[unbatchedToken.Token])

// Simplified: totalFee > (gasCost * profitMultiplier).
profitMult := decimal.NewFromFloat(p.relayer.GetProfitMultiplier())
shouldRequestBatch = totalFeeInUSDDec.GreaterThanOrEqual(gasCostInUSDDec.Mul(profitMult))
}

if err := p.gravityBroadcastClient.SendRequestBatch(ctx, denom); err != nil {
logger.Err(err).Msg("failed to send batch request")
if shouldRequestBatch {
logger.Info().Str("token_contract", tokenAddr.String()).Str("denom", denom).Msg("sending batch request")

if err := p.gravityBroadcastClient.SendRequestBatch(ctx, denom); err != nil {
logger.Err(err).Msg("failed to send batch request")
}
} else {
logger.Debug().
Str("token_contract", tokenAddr.String()).
Str("denom", denom).
Msg("not profitable yet, skipping batch creation")
}
}

Expand Down
1 change: 1 addition & 0 deletions orchestrator/oracle_resync_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ func TestGetLastCheckedBlock(t *testing.T) {
time.Second,
100,
0,
nil,
)

block, err := orch.GetLastCheckedBlock(context.Background(), 0)
Expand Down
4 changes: 4 additions & 0 deletions orchestrator/orchestrator.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/rs/zerolog"
gravitytypes "github.com/umee-network/Gravity-Bridge/module/x/gravity/types"

"github.com/umee-network/peggo/orchestrator/coingecko"
sidechain "github.com/umee-network/peggo/orchestrator/cosmos"
gravity "github.com/umee-network/peggo/orchestrator/ethereum/gravity"
"github.com/umee-network/peggo/orchestrator/ethereum/keystore"
Expand Down Expand Up @@ -41,6 +42,7 @@ type gravityOrchestrator struct {
batchRequesterLoopDuration time.Duration
ethBlocksPerLoop uint64
bridgeStartHeight uint64
priceFeeder *coingecko.PriceFeed

mtx sync.Mutex
erc20DenomCache map[string]string
Expand All @@ -60,6 +62,7 @@ func NewGravityOrchestrator(
batchRequesterLoopDuration time.Duration,
ethBlocksPerLoop int64,
bridgeStartHeight int64,
priceFeeder *coingecko.PriceFeed,
options ...func(GravityOrchestrator),
) GravityOrchestrator {

Expand All @@ -78,6 +81,7 @@ func NewGravityOrchestrator(
batchRequesterLoopDuration: batchRequesterLoopDuration,
ethBlocksPerLoop: uint64(ethBlocksPerLoop),
bridgeStartHeight: uint64(bridgeStartHeight),
priceFeeder: priceFeeder,
}

for _, option := range options {
Expand Down
3 changes: 2 additions & 1 deletion orchestrator/relayer/batch_relaying.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
ethcmn "github.com/ethereum/go-ethereum/common"
"github.com/shopspring/decimal"
"github.com/umee-network/Gravity-Bridge/module/x/gravity/types"
"github.com/umee-network/peggo/orchestrator/coingecko"
)

type SubmittableBatch struct {
Expand Down Expand Up @@ -220,7 +221,7 @@ func (s *gravityRelayer) IsBatchProfitable(
}

// First we get the cost of the transaction in USD
usdEthPrice, err := s.priceFeeder.QueryUSDPriceByCoinID("ethereum")
usdEthPrice, err := s.priceFeeder.QueryUSDPriceByCoinID(coingecko.EthereumCoinID)
if err != nil {
s.logger.Err(err).Msg("failed to get ETH price")
return false
Expand Down
6 changes: 6 additions & 0 deletions orchestrator/relayer/relayer.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ type GravityRelayer interface {
// SetPriceFeeder sets the (optional) price feeder used when performing profitable
// batch calculations.
SetPriceFeeder(*coingecko.PriceFeed)

GetProfitMultiplier() float64
}

type gravityRelayer struct {
Expand Down Expand Up @@ -94,3 +96,7 @@ func NewGravityRelayer(

return relayer
}

func (s *gravityRelayer) GetProfitMultiplier() float64 {
return s.profitMultiplier
}

0 comments on commit 996e522

Please sign in to comment.