diff --git a/node/pkg/db/governor.go b/node/pkg/db/governor.go index 98f6c2ca10..48bcc9d0e5 100644 --- a/node/pkg/db/governor.go +++ b/node/pkg/db/governor.go @@ -45,10 +45,15 @@ func (d *MockGovernorDB) GetChainGovernorData(logger *zap.Logger) (transfers []* } type Transfer struct { - Timestamp time.Time - Value uint64 - OriginChain vaa.ChainID - OriginAddress vaa.Address + // This value is generated by the Governor. It is not read from the blockchain transaction. It represents the + // time at which it was observed and evaluated by the Governor. + Timestamp time.Time + // Notional USD value of the transfer + Value uint64 + // Where the asset was minted + OriginChain vaa.ChainID + OriginAddress vaa.Address + // Where the transfer was emitted. Not necessarily equal to OriginChain EmitterChain vaa.ChainID EmitterAddress vaa.Address MsgID string diff --git a/node/pkg/governor/devnet_config.go b/node/pkg/governor/devnet_config.go index c42e310c4f..55a6e2f68b 100644 --- a/node/pkg/governor/devnet_config.go +++ b/node/pkg/governor/devnet_config.go @@ -6,20 +6,25 @@ import ( "github.com/wormhole-foundation/wormhole/sdk/vaa" ) -func (gov *ChainGovernor) initDevnetConfig() ([]tokenConfigEntry, []chainConfigEntry) { +func (gov *ChainGovernor) initDevnetConfig() ([]tokenConfigEntry, []tokenConfigEntry, []chainConfigEntry) { gov.logger.Info("setting up devnet config") gov.dayLengthInMinutes = 5 tokens := []tokenConfigEntry{ - tokenConfigEntry{chain: 1, addr: "069b8857feab8184fb687f634618c035dac439dc1aeb3b5598a0f00000000001", symbol: "SOL", coinGeckoId: "wrapped-solana", decimals: 8, price: 34.94}, // Addr: So11111111111111111111111111111111111111112, Notional: 4145006 - tokenConfigEntry{chain: 2, addr: "000000000000000000000000DDb64fE46a91D46ee29420539FC25FD07c5FEa3E", symbol: "WETH", coinGeckoId: "weth", decimals: 8, price: 1174}, + {chain: 1, addr: "069b8857feab8184fb687f634618c035dac439dc1aeb3b5598a0f00000000001", symbol: "SOL", coinGeckoId: "wrapped-solana", decimals: 8, price: 34.94}, // Addr: So11111111111111111111111111111111111111112, Notional: 4145006 + {chain: 1, addr: "3b442cb3912157f13a933d0134282d032b5ffecd01a2dbf1b7790608df002ea7", symbol: "USDC", coinGeckoId: "usdc", decimals: 6, price: 1}, // Addr: 4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU, Notional: 1 + {chain: 2, addr: "000000000000000000000000DDb64fE46a91D46ee29420539FC25FD07c5FEa3E", symbol: "WETH", coinGeckoId: "weth", decimals: 8, price: 1174}, + } + + flowCancelTokens := []tokenConfigEntry{ + {chain: 1, addr: "3b442cb3912157f13a933d0134282d032b5ffecd01a2dbf1b7790608df002ea7", symbol: "USDC", coinGeckoId: "usdc", decimals: 6, price: 1}, // Addr: 4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU, Notional: 1 } chains := []chainConfigEntry{ - chainConfigEntry{emitterChainID: vaa.ChainIDSolana, dailyLimit: 100, bigTransactionSize: 75}, - chainConfigEntry{emitterChainID: vaa.ChainIDEthereum, dailyLimit: 100000}, + {emitterChainID: vaa.ChainIDSolana, dailyLimit: 100, bigTransactionSize: 75}, + {emitterChainID: vaa.ChainIDEthereum, dailyLimit: 100000}, } - return tokens, chains + return tokens, flowCancelTokens, chains } diff --git a/node/pkg/governor/flow_cancel_tokens.go b/node/pkg/governor/flow_cancel_tokens.go new file mode 100644 index 0000000000..c4d95c150a --- /dev/null +++ b/node/pkg/governor/flow_cancel_tokens.go @@ -0,0 +1,24 @@ +package governor + +// FlowCancelTokenList returns a list of `tokenConfigEntry`s representing tokens that can 'Flow Cancel'. This means that incoming transfers +// that use these tokens can reduce the 'daily usage' of the Governor configured for the destination chain. +// The list of tokens was generated by grepping the file `generated_mainnet_tokens.go` for "USDC", "USDT", and "DAI". +// +// Only tokens that are configured in the mainnet token list should be able to flow cancel. That is, if a token is +// present in this list but not in the mainnet token lists, it should not flow cancel. +// +// Note that the field `symbol` is unused. It is retained in this file only for convenience. +func FlowCancelTokenList() []tokenConfigEntry { + return []tokenConfigEntry{ + // USDC variants + {chain: 2, addr: "000000000000000000000000bcca60bb61934080951369a648fb03df4f96263c", symbol: "aUSDC"}, + + // USDT variants + {chain: 1, addr: "b7db4e83eb727f1187bd7a50303f5b4e4e943503da8571ad6564a51131504792", symbol: ""}, + {chain: 1, addr: "ce010e60afedb22717bd63192f54145a3f965a33bb82d2c7029eb2ce1e208264", symbol: "USDT"}, + {chain: 2, addr: "000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec7", symbol: "USDT"}, + + // DAI variants + {chain: 2, addr: "0000000000000000000000006b175474e89094c44da98b954eedeac495271d0f", symbol: "DAI"}, + } +} diff --git a/node/pkg/governor/governor.go b/node/pkg/governor/governor.go index 325b3666d2..017b8f88e2 100644 --- a/node/pkg/governor/governor.go +++ b/node/pkg/governor/governor.go @@ -1,3 +1,5 @@ +package governor + // The purpose of the Chain Governor is to limit the notional TVL that can leave a chain in a single day. // It works by tracking transfers (types one and three) for a configured set of tokens from a configured set of emitters (chains). // @@ -23,8 +25,6 @@ // // To enable the chain governor, you must specified the --chainGovernorEnabled guardiand command line argument. -package governor - import ( "context" "encoding/hex" @@ -83,6 +83,7 @@ type ( cfgPrice *big.Float coinGeckoPrice *big.Float priceTime time.Time + flowCancels bool } // Payload for each enqueued transfer @@ -93,7 +94,17 @@ type ( dbData db.PendingTransfer // This info gets persisted in the DB. } - // Payload of the map of chains being monitored + // Used in flow cancel calculations. Wraps a database Transfer. Also contains a signed amount field in order to + // hold negative values. This field will be used in flow cancel calculations to reduce the Governor usage for a + // supported token. + transfer struct { + dbTransfer *db.Transfer + value int64 + } + + // Payload of the map of chains being monitored. Contains transfer data for both emitted and received transfers. + // `transfers` with positive Value represent outgoing transfers from the emitterChainId. Transfers with negative + // Value represent incoming transfers of Assets that can Flow Cancel. chainEntry struct { emitterChainId vaa.ChainID emitterAddr vaa.Address @@ -101,11 +112,68 @@ type ( bigTransactionSize uint64 checkForBigTransactions bool - transfers []*db.Transfer + transfers []transfer pending []*pendingEntry } ) +// newTransferFromDbTransfer performs a bounds check on dbTransfer.Value to ensure it can fit into int64. +// This should always be the case for normal operation as dbTransfer.Value represents the USD value of a transfer. +func newTransferFromDbTransfer(dbTransfer *db.Transfer) (tx transfer, err error) { + if dbTransfer.Value > math.MaxInt64 { + return tx, fmt.Errorf("value for db.Transfer exceeds MaxInt64: %d", dbTransfer.Value) + } + return transfer{dbTransfer, int64(dbTransfer.Value)}, nil +} + +// addFlowCancelTransfer appends a transfer to a ChainEntry's transfers property. +// SECURITY: The calling code is responsible for ensuring that the asset within the transfer is a flow-cancelling asset. +// SECURITY: This method performs validation to ensure that the Flow Cancel transfer is valid. This is important to +// ensure that the Governor usage cannot be lowered due to malicious or invalid transfers. +// - the Value must be negative (in order to represent an incoming value) +// - the TargetChain must match the chain ID of the Chain Entry +func (ce *chainEntry) addFlowCancelTransfer(transfer transfer) error { + value := transfer.value + targetChain := transfer.dbTransfer.TargetChain + if value > 0 { + return fmt.Errorf("flow cancel transfer Value must be negative. Value: %d", value) + } + if transfer.dbTransfer.Value > math.MaxInt64 { + return fmt.Errorf("value for transfer.dbTransfer exceeds MaxInt64: %d", transfer.dbTransfer.Value) + } + // Type conversion is safe here because of the MaxInt64 bounds check above + if value != -int64(transfer.dbTransfer.Value) { + return fmt.Errorf("transfer is invalid: transfer.value %d must equal the inverse of transfer.dbTransfer.Value %d", value, transfer.dbTransfer.Value) + } + if targetChain != ce.emitterChainId { + return fmt.Errorf("flow cancel transfer TargetChain %s does not match this chainEntry %s", targetChain, ce.emitterChainId) + } + + ce.transfers = append(ce.transfers, transfer) + return nil +} + +// addFlowCancelTransferFromDbTransfer converts a dbTransfer to a transfer and adds it to the +// Chain Entry. +// Validation of transfer data is performed by other methods: see addFlowCancelTransfer, newTransferFromDbTransfer. +func (ce *chainEntry) addFlowCancelTransferFromDbTransfer(dbTransfer *db.Transfer) error { + transfer, err := newTransferFromDbTransfer(dbTransfer) + if err != nil { + return err + } + err = ce.addFlowCancelTransfer(transfer.inverse()) + if err != nil { + return err + } + return nil +} + +// inverse takes a transfer and returns a copy of that transfer with the +// additive inverse of its Value property (i.e. flip the sign). +func (t *transfer) inverse() transfer { + return transfer{t.dbTransfer, -t.value} +} + func (ce *chainEntry) isBigTransfer(value uint64) bool { return value >= ce.bigTransactionSize && ce.checkForBigTransactions } @@ -170,12 +238,13 @@ func (gov *ChainGovernor) initConfig() error { gov.dayLengthInMinutes = 24 * 60 configTokens := tokenList() + flowCancelTokens := FlowCancelTokenList() configChains := chainList() if gov.env == common.UnsafeDevNet { - configTokens, configChains = gov.initDevnetConfig() + configTokens, flowCancelTokens, configChains = gov.initDevnetConfig() } else if gov.env == common.TestNet { - configTokens, configChains = gov.initTestnetConfig() + configTokens, flowCancelTokens, configChains = gov.initTestnetConfig() } for _, ct := range configTokens { @@ -204,7 +273,14 @@ func (gov *ChainGovernor) initConfig() error { } key := tokenKey{chain: vaa.ChainID(ct.chain), addr: addr} - te := &tokenEntry{cfgPrice: cfgPrice, price: initialPrice, decimals: decimals, symbol: symbol, coinGeckoId: ct.coinGeckoId, token: key} + te := &tokenEntry{ + cfgPrice: cfgPrice, + price: initialPrice, + decimals: decimals, + symbol: symbol, + coinGeckoId: ct.coinGeckoId, + token: key, + } te.updatePrice() gov.tokens[key] = te @@ -230,6 +306,26 @@ func (gov *ChainGovernor) initConfig() error { } } + for _, flowCancelConfigEntry := range flowCancelTokens { + addr, err := vaa.StringToAddress(flowCancelConfigEntry.addr) + if err != nil { + return err + } + key := tokenKey{chain: vaa.ChainID(flowCancelConfigEntry.chain), addr: addr} + + // Only add flow cancelling for tokens that are already configured for rate-limiting. + if _, ok := gov.tokens[key]; ok { + gov.tokens[key].flowCancels = true + } else { + gov.logger.Debug("token present in flow cancel list but absent from main token list:", + zap.Stringer("chain", key.chain), + zap.Stringer("addr", key.addr), + zap.String("symbol", flowCancelConfigEntry.symbol), + zap.String("coinGeckoId", flowCancelConfigEntry.coinGeckoId), + ) + } + } + if len(gov.tokens) == 0 { return fmt.Errorf("no tokens are configured") } @@ -293,6 +389,11 @@ func (gov *ChainGovernor) ProcessMsg(msg *common.MessagePublication) bool { return publish } +// ProcessMsgForTime handles an incoming message (transfer) and registers it in the chain entries for the Governor. +// Validation: +// - ensure MessagePublication is not nil +// - check that the MessagePublication is governed +// - check that the message is not a duplicate of one we've seen before. func (gov *ChainGovernor) ProcessMsgForTime(msg *common.MessagePublication, now time.Time) (bool, error) { if msg == nil { return false, fmt.Errorf("msg is nil") @@ -301,7 +402,8 @@ func (gov *ChainGovernor) ProcessMsgForTime(msg *common.MessagePublication, now gov.mutex.Lock() defer gov.mutex.Unlock() - msgIsGoverned, ce, token, payload, err := gov.parseMsgAlreadyLocked(msg) + msgIsGoverned, emitterChainEntry, token, payload, err := gov.parseMsgAlreadyLocked(msg) + if err != nil { return false, err } @@ -330,10 +432,11 @@ func (gov *ChainGovernor) ProcessMsgForTime(msg *common.MessagePublication, now return true, nil } + // Get all outgoing transfers for `emitterChainEntry` that happened within the last 24 hours startTime := now.Add(-time.Minute * time.Duration(gov.dayLengthInMinutes)) - prevTotalValue, err := gov.TrimAndSumValueForChain(ce, startTime) + prevTotalValue, err := gov.TrimAndSumValueForChain(emitterChainEntry, startTime) if err != nil { - gov.logger.Error("failed to trim transfers", + gov.logger.Error("Error when attempting to trim and sum transfers", zap.String("msgID", msg.MessageIDString()), zap.String("hash", hash), zap.Stringer("txHash", msg.TxHash), @@ -342,6 +445,7 @@ func (gov *ChainGovernor) ProcessMsgForTime(msg *common.MessagePublication, now return false, err } + // Compute the notional USD value of the transfers value, err := computeValue(payload.Amount, token) if err != nil { gov.logger.Error("failed to compute value of transfer", @@ -367,7 +471,7 @@ func (gov *ChainGovernor) ProcessMsgForTime(msg *common.MessagePublication, now enqueueIt := false var releaseTime time.Time - if ce.isBigTransfer(value) { + if emitterChainEntry.isBigTransfer(value) { enqueueIt = true releaseTime = now.Add(maxEnqueuedTime) gov.logger.Error("enqueuing vaa because it is a big transaction", @@ -376,11 +480,11 @@ func (gov *ChainGovernor) ProcessMsgForTime(msg *common.MessagePublication, now zap.Uint64("newTotalValue", newTotalValue), zap.String("msgID", msg.MessageIDString()), zap.Stringer("releaseTime", releaseTime), - zap.Uint64("bigTransactionSize", ce.bigTransactionSize), + zap.Uint64("bigTransactionSize", emitterChainEntry.bigTransactionSize), zap.String("hash", hash), zap.Stringer("txHash", msg.TxHash), ) - } else if newTotalValue > ce.dailyLimit { + } else if newTotalValue > emitterChainEntry.dailyLimit { enqueueIt = true releaseTime = now.Add(maxEnqueuedTime) gov.logger.Error("enqueuing vaa because it would exceed the daily limit", @@ -407,7 +511,10 @@ func (gov *ChainGovernor) ProcessMsgForTime(msg *common.MessagePublication, now return false, err } - ce.pending = append(ce.pending, &pendingEntry{token: token, amount: payload.Amount, hash: hash, dbData: dbData}) + emitterChainEntry.pending = append( + emitterChainEntry.pending, + &pendingEntry{token: token, amount: payload.Amount, hash: hash, dbData: dbData}, + ) gov.msgsSeen[hash] = transferEnqueued return false, nil } @@ -421,7 +528,8 @@ func (gov *ChainGovernor) ProcessMsgForTime(msg *common.MessagePublication, now zap.Stringer("txHash", msg.TxHash), ) - xfer := db.Transfer{Timestamp: now, + dbTransfer := db.Transfer{ + Timestamp: now, Value: value, OriginChain: token.token.chain, OriginAddress: token.token.addr, @@ -432,7 +540,8 @@ func (gov *ChainGovernor) ProcessMsgForTime(msg *common.MessagePublication, now MsgID: msg.MessageIDString(), Hash: hash, } - err = gov.db.StoreTransfer(&xfer) + + err = gov.db.StoreTransfer(&dbTransfer) if err != nil { gov.logger.Error("failed to store transfer", zap.String("msgID", msg.MessageIDString()), @@ -442,7 +551,41 @@ func (gov *ChainGovernor) ProcessMsgForTime(msg *common.MessagePublication, now return false, err } - ce.transfers = append(ce.transfers, &xfer) + transfer, err := newTransferFromDbTransfer(&dbTransfer) + if err != nil { + return false, err + } + + // Update the chainEntries. For the emitter chain, add the transfer so that it can be factored into calculating + // the usage of this chain the next time that the Governor processes a transfer. + // For the destination chain entry, add the inverse of this transfer. + // e.g. A transfer of USDC originally minted on Solana is sent from Ethereum to Sui. + // - This increases the Governor usage of Ethereum by the `transfer.Value` amount. + // - If the USDC version of Solana is flow cancelled, we also want to decrease the Governor usage for Sui. + // - We do this by adding an 'inverse' transfer to Sui's chainEntry that contains a negative `transfer.Value`. + // - This will cause the summed value of Sui to decrease. + emitterChainEntry.transfers = append(emitterChainEntry.transfers, transfer) + + // Add inverse transfer to destination chain entry if this asset can cancel flows. + key := tokenKey{chain: msg.EmitterChain, addr: msg.EmitterAddress} + tokenEntry := gov.tokens[key] + if tokenEntry != nil { + // Mandatory check to ensure that the token should be able to reduce the Governor limit. + if tokenEntry.flowCancels { + if destinationChainEntry, ok := gov.chains[payload.TargetChain]; ok { + if err := destinationChainEntry.addFlowCancelTransferFromDbTransfer(&dbTransfer); err != nil { + return false, err + } + } else { + gov.logger.Warn("tried to cancel flow but chain entry for target chain does not exist", + zap.String("msgID", msg.MessageIDString()), + zap.String("hash", hash), zap.Error(err), + zap.Stringer("target chain", payload.TargetChain), + ) + } + } + } + gov.msgsSeen[hash] = transferComplete return true, nil } @@ -456,19 +599,27 @@ func (gov *ChainGovernor) IsGovernedMsg(msg *common.MessagePublication) (msgIsGo } // parseMsgAlreadyLocked determines if the message applies to the governor and also returns data useful to the governor. It assumes the caller holds the lock. -func (gov *ChainGovernor) parseMsgAlreadyLocked(msg *common.MessagePublication) (bool, *chainEntry, *tokenEntry, *vaa.TransferPayloadHdr, error) { +func (gov *ChainGovernor) parseMsgAlreadyLocked( + msg *common.MessagePublication, +) (bool, *chainEntry, *tokenEntry, *vaa.TransferPayloadHdr, error) { // If we don't care about this chain, the VAA can be published. ce, exists := gov.chains[msg.EmitterChain] if !exists { if msg.EmitterChain != vaa.ChainIDPythNet { - gov.logger.Info("ignoring vaa because the emitter chain is not configured", zap.String("msgID", msg.MessageIDString())) + gov.logger.Info( + "ignoring vaa because the emitter chain is not configured", + zap.String("msgID", msg.MessageIDString()), + ) } return false, nil, nil, nil, nil } // If we don't care about this emitter, the VAA can be published. if msg.EmitterAddress != ce.emitterAddr { - gov.logger.Info("ignoring vaa because the emitter address is not configured", zap.String("msgID", msg.MessageIDString())) + gov.logger.Info( + "ignoring vaa because the emitter address is not configured", + zap.String("msgID", msg.MessageIDString()), + ) return false, nil, nil, nil, nil } @@ -519,7 +670,7 @@ func (gov *ChainGovernor) CheckPendingForTime(now time.Time) ([]*common.MessageP foundOne := false prevTotalValue, err := gov.TrimAndSumValueForChain(ce, startTime) if err != nil { - gov.logger.Error("failed to trim transfers", zap.Error(err)) + gov.logger.Error("error when attempting to trim and sum transfers", zap.Error(err)) gov.msgsToPublish = msgsToPublish return nil, err } @@ -594,7 +745,7 @@ func (gov *ChainGovernor) CheckPendingForTime(now time.Time) ([]*common.MessageP msgsToPublish = append(msgsToPublish, &pe.dbData.Msg) if countsTowardsTransfers { - xfer := db.Transfer{Timestamp: now, + dbTransfer := db.Transfer{Timestamp: now, Value: value, OriginChain: pe.token.token.chain, OriginAddress: pe.token.token.addr, @@ -606,12 +757,36 @@ func (gov *ChainGovernor) CheckPendingForTime(now time.Time) ([]*common.MessageP Hash: pe.hash, } - if err := gov.db.StoreTransfer(&xfer); err != nil { + if err := gov.db.StoreTransfer(&dbTransfer); err != nil { gov.msgsToPublish = msgsToPublish return nil, err } - ce.transfers = append(ce.transfers, &xfer) + transfer, err := newTransferFromDbTransfer(&dbTransfer) + if err != nil { + return nil, err + } + ce.transfers = append(ce.transfers, transfer) + + // Add inverse transfer to destination chain entry if this asset can cancel flows. + key := tokenKey{chain: dbTransfer.EmitterChain, addr: dbTransfer.EmitterAddress} + tokenEntry := gov.tokens[key] + if tokenEntry != nil { + // Mandatory check to ensure that the token should be able to reduce the Governor limit. + if tokenEntry.flowCancels { + if destinationChainEntry, ok := gov.chains[payload.TargetChain]; ok { + if err := destinationChainEntry.addFlowCancelTransferFromDbTransfer(&dbTransfer); err != nil { + return nil, err + } + } else { + gov.logger.Warn("tried to cancel flow but chain entry for target chain does not exist", + zap.String("msgID", dbTransfer.MsgID), + zap.String("hash", pe.hash), zap.Error(err), + zap.Stringer("target chain", payload.TargetChain), + ) + } + } + } gov.msgsSeen[pe.hash] = transferComplete } else { delete(gov.msgsSeen, pe.hash) @@ -656,34 +831,82 @@ func computeValue(amount *big.Int, token *tokenEntry) (uint64, error) { return value, nil } -func (gov *ChainGovernor) TrimAndSumValueForChain(ce *chainEntry, startTime time.Time) (sum uint64, err error) { - sum, ce.transfers, err = gov.TrimAndSumValue(ce.transfers, startTime) - return sum, err +// TrimAndSumValueForChain calculates the `sum` of `Transfer`s for a given chain `emitter`. In effect, it represents a +// chain's "Governor Usage" for a given 24 hour period. +// This sum may be reduced by the sum of 'flow cancelling' transfers: that is, transfers of an allow-listed token +// that have the `emitter` as their destination chain. +// The resulting `sum` return value therefore represents the net flow across a chain when taking flow-cancelling tokens +// into account. Therefore, this value should never be less than 0 and should never exceed the "Governor limit" for the chain. +// As a side-effect, this function modifies the parameter `emitter`, updating its `transfers` field so that it only includes +// filtered `Transfer`s (i.e. outgoing `Transfer`s newer than `startTime`). +// SECURITY Invariant: The `sum` return value should never be less than 0 +// SECURITY Invariant: The `sum` return value should never exceed the "Governor limit" for the chain +func (gov *ChainGovernor) TrimAndSumValueForChain(emitter *chainEntry, startTime time.Time) (sum uint64, err error) { + // Sum the value of all outgoing transfers + var sumOutgoing int64 + sumOutgoing, emitter.transfers, err = gov.TrimAndSumValue(emitter.transfers, startTime) + if err != nil { + return 0, err + } + + // Return early if the sum is not positive as it cannot exceed the daily limit. + // In this case, return 0 even if the sum is negative. + if sumOutgoing <= 0 { + return 0, nil + } + + sum = uint64(sumOutgoing) + if sum > emitter.dailyLimit { + return 0, fmt.Errorf( + "invariant violation: calculated sum %d exceeds Governor limit %d", + sum, + emitter.dailyLimit, + ) + } + + return sum, nil + } -func (gov *ChainGovernor) TrimAndSumValue(transfers []*db.Transfer, startTime time.Time) (uint64, []*db.Transfer, error) { +// TrimAndSumValue iterates over a slice of transfer structs. It filters out transfers that have Timestamp values that +// are earlier than the parameter `startTime`. The function then iterates over the remaining transfers, sums their Value, +// and returns the sum and the filtered transfers. +// The `transfers` slice must be sorted by Timestamp. We expect this to be the case as transfers are added to the +// Governor in chronological order as they arrive. Note that `Timestamp` is created by the Governor; it is not read +// from the actual on-chain transaction. +func (gov *ChainGovernor) TrimAndSumValue(transfers []transfer, startTime time.Time) (int64, []transfer, error) { if len(transfers) == 0 { return 0, transfers, nil } var trimIdx int = -1 - var sum uint64 + var sum int64 for idx, t := range transfers { - if t.Timestamp.Before(startTime) { + if t.dbTransfer.Timestamp.Before(startTime) { trimIdx = idx } else { - sum += t.Value + checkedSum, err := CheckedAddInt64(sum, t.value) + if err != nil { + // We have to stop and return an error here (rather than saturate, for example). The + // transfers are not sorted by value so we can't make any guarantee on the final value + // if we hit the upper or lower bound. We don't expect this to happen in any case + // because we don't expect this number to ever overflow, as it would represent + // $184467440737095516.15 USD moving between two chains in a 24h period. + return 0, transfers, err + } + sum = checkedSum } } if trimIdx >= 0 { for idx := 0; idx <= trimIdx; idx++ { - if err := gov.db.DeleteTransfer(transfers[idx]); err != nil { + dbTransfer := transfers[idx].dbTransfer + if err := gov.db.DeleteTransfer(dbTransfer); err != nil { return 0, transfers, err } - delete(gov.msgsSeen, transfers[idx].Hash) + delete(gov.msgsSeen, dbTransfer.Hash) } transfers = transfers[trimIdx+1:] @@ -701,3 +924,48 @@ func (gov *ChainGovernor) HashFromMsg(msg *common.MessagePublication) string { digest := v.SigningDigest() return hex.EncodeToString(digest.Bytes()) } + +// CheckedAddUint64 adds two uint64 values with overflow checks +func CheckedAddUint64(x uint64, y uint64) (uint64, error) { + if x == 0 { + return y, nil + } + if y == 0 { + return x, nil + } + + sum := x + y + + if sum < x || sum < y { + return 0, fmt.Errorf("integer overflow when adding %d and %d", x, y) + } + + return sum, nil +} + +// CheckedAddInt64 adds two uint64 values with overflow checks +func CheckedAddInt64(x int64, y int64) (int64, error) { + if x == 0 { + return y, nil + } + if y == 0 { + return x, nil + } + + sum := x + y + + // Both terms positive - overflow check + if x > 0 && y > 0 { + if sum < x || sum < y { + return 0, fmt.Errorf("integer overflow when adding %d and %d", x, y) + } + } + + // Both terms negative - underflow check + if x < 0 && y < 0 { + if sum > x || sum > y { + return 0, fmt.Errorf("integer underflow when adding %d and %d", x, y) + } + } + return x + y, nil +} diff --git a/node/pkg/governor/governor_db.go b/node/pkg/governor/governor_db.go index e3e976687f..146f2c26f9 100644 --- a/node/pkg/governor/governor_db.go +++ b/node/pkg/governor/governor_db.go @@ -44,7 +44,9 @@ func (gov *ChainGovernor) loadFromDBAlreadyLocked() error { startTime := now.Add(-time.Minute * time.Duration(gov.dayLengthInMinutes)) for _, xfer := range xfers { if startTime.Before(xfer.Timestamp) { - gov.reloadTransfer(xfer) + if err := gov.reloadTransfer(xfer); err != nil { + return err + } } else { if err := gov.db.DeleteTransfer(xfer); err != nil { return err @@ -156,7 +158,7 @@ func (gov *ChainGovernor) reloadPendingTransfer(pending *db.PendingTransfer) { gov.msgsSeen[hash] = transferEnqueued } -func (gov *ChainGovernor) reloadTransfer(xfer *db.Transfer) { +func (gov *ChainGovernor) reloadTransfer(xfer *db.Transfer) error { ce, exists := gov.chains[xfer.EmitterChain] if !exists { gov.logger.Error("reloaded transfer for unsupported chain, dropping it", @@ -166,7 +168,7 @@ func (gov *ChainGovernor) reloadTransfer(xfer *db.Transfer) { zap.Stringer("EmitterAddress", xfer.EmitterAddress), zap.String("MsgID", xfer.MsgID), ) - return + return nil } if xfer.EmitterAddress != ce.emitterAddr { @@ -177,7 +179,7 @@ func (gov *ChainGovernor) reloadTransfer(xfer *db.Transfer) { zap.Stringer("OriginAddress", xfer.OriginAddress), zap.String("MsgID", xfer.MsgID), ) - return + return nil } tk := tokenKey{chain: xfer.OriginChain, addr: xfer.OriginAddress} @@ -190,7 +192,7 @@ func (gov *ChainGovernor) reloadTransfer(xfer *db.Transfer) { zap.Stringer("OriginAddress", xfer.OriginAddress), zap.String("MsgID", xfer.MsgID), ) - return + return nil } if _, alreadyExists := gov.msgsSeen[xfer.Hash]; alreadyExists { @@ -202,7 +204,7 @@ func (gov *ChainGovernor) reloadTransfer(xfer *db.Transfer) { zap.String("MsgID", xfer.MsgID), zap.String("Hash", xfer.Hash), ) - return + return nil } if xfer.Hash != "" { @@ -226,5 +228,10 @@ func (gov *ChainGovernor) reloadTransfer(xfer *db.Transfer) { ) } - ce.transfers = append(ce.transfers, xfer) + transfer, err := newTransferFromDbTransfer(xfer) + if err != nil { + return err + } + ce.transfers = append(ce.transfers, transfer) + return nil } diff --git a/node/pkg/governor/governor_monitoring.go b/node/pkg/governor/governor_monitoring.go index 2cbaa747c8..b43e8e5ba3 100644 --- a/node/pkg/governor/governor_monitoring.go +++ b/node/pkg/governor/governor_monitoring.go @@ -80,7 +80,6 @@ import ( "sort" "time" - "github.com/certusone/wormhole/node/pkg/db" gossipv1 "github.com/certusone/wormhole/node/pkg/proto/gossip/v1" publicrpcv1 "github.com/certusone/wormhole/node/pkg/proto/publicrpc/v1" "github.com/wormhole-foundation/wormhole/sdk/vaa" @@ -96,14 +95,19 @@ import ( ) // Admin command to display status to the log. -func (gov *ChainGovernor) Status() string { +func (gov *ChainGovernor) Status() (resp string) { gov.mutex.Lock() defer gov.mutex.Unlock() startTime := time.Now().Add(-time.Minute * time.Duration(gov.dayLengthInMinutes)) - var resp string for _, ce := range gov.chains { - valueTrans := sumValue(ce.transfers, startTime) + valueTrans, err := sumValue(ce.transfers, startTime) + if err != nil { + // We don't want to actually return an error or otherwise stop + // execution in this case. Instead of propagating the error here, print the contents of the + // error message. + return fmt.Sprintf("chain: %v, dailyLimit: OVERFLOW. error: %s", ce.emitterChainId, err) + } s1 := fmt.Sprintf("chain: %v, dailyLimit: %v, total: %v, numPending: %v", ce.emitterChainId, ce.dailyLimit, valueTrans, len(ce.pending)) resp += s1 + "\n" gov.logger.Info(s1) @@ -242,32 +246,48 @@ func (gov *ChainGovernor) resetReleaseTimerForTime(vaaId string, now time.Time) return "", fmt.Errorf("vaa not found in the pending list") } -func sumValue(transfers []*db.Transfer, startTime time.Time) uint64 { +// sumValue sums the value of all `transfers`. See also `TrimAndSumValue`. +func sumValue(transfers []transfer, startTime time.Time) (uint64, error) { if len(transfers) == 0 { - return 0 + return 0, nil } - var sum uint64 + var sum int64 for _, t := range transfers { - if !t.Timestamp.Before(startTime) { - sum += t.Value + if t.dbTransfer.Timestamp.Before(startTime) { + continue + } + checkedSum, err := CheckedAddInt64(sum, t.value) + if err != nil { + // We have to stop and return an error here (rather than saturate, for example). The + // transfers are not sorted by value so we can't make any guarantee on the final value + // if we hit the upper or lower bound. We don't expect this to happen in any case. + return 0, err } + sum = checkedSum } - return sum + // Do not return negative values. Instead, saturate to zero. + if sum <= 0 { + return 0, nil + } + + return uint64(sum), nil } // REST query to get the current available notional value per chain. -func (gov *ChainGovernor) GetAvailableNotionalByChain() []*publicrpcv1.GovernorGetAvailableNotionalByChainResponse_Entry { +func (gov *ChainGovernor) GetAvailableNotionalByChain() (resp []*publicrpcv1.GovernorGetAvailableNotionalByChainResponse_Entry) { gov.mutex.Lock() defer gov.mutex.Unlock() - resp := make([]*publicrpcv1.GovernorGetAvailableNotionalByChainResponse_Entry, 0) - startTime := time.Now().Add(-time.Minute * time.Duration(gov.dayLengthInMinutes)) for _, ce := range gov.chains { - value := sumValue(ce.transfers, startTime) + value, err := sumValue(ce.transfers, startTime) + if err != nil { + // Don't return an error here, just return 0 + return make([]*publicrpcv1.GovernorGetAvailableNotionalByChainResponse_Entry, 0) + } if value >= ce.dailyLimit { value = 0 } else { @@ -421,7 +441,12 @@ func (gov *ChainGovernor) CollectMetrics(hb *gossipv1.Heartbeat, sendC chan<- [] if exists { enabled = "1" - value := sumValue(ce.transfers, startTime) + value, err := sumValue(ce.transfers, startTime) + if err != nil { + // Error can occur if the sum overflows. Return 0 in this case rather than returning an + // error. + value = 0 + } if value >= ce.dailyLimit { value = 0 } else { @@ -527,8 +552,12 @@ func (gov *ChainGovernor) publishStatus(hb *gossipv1.Heartbeat, sendC chan<- []b chains := make([]*gossipv1.ChainGovernorStatus_Chain, 0) numEnqueued := 0 for _, ce := range gov.chains { - value := sumValue(ce.transfers, startTime) - if value >= ce.dailyLimit { + value, err := sumValue(ce.transfers, startTime) + + if err != nil || value >= ce.dailyLimit { + // In case of error, set value to 0 rather than returning an error to the caller. An error + // here means sumValue has encountered an overflow and this should never happen. Even if it did + // we don't want to stop execution here. value = 0 } else { value = ce.dailyLimit - value diff --git a/node/pkg/governor/governor_test.go b/node/pkg/governor/governor_test.go index 871a7fadc0..3faf3799ba 100644 --- a/node/pkg/governor/governor_test.go +++ b/node/pkg/governor/governor_test.go @@ -49,7 +49,12 @@ func (gov *ChainGovernor) setDayLengthInMinutes(min int) { gov.dayLengthInMinutes = min } -func (gov *ChainGovernor) setChainForTesting(emitterChainId vaa.ChainID, emitterAddrStr string, dailyLimit uint64, bigTransactionSize uint64) error { +func (gov *ChainGovernor) setChainForTesting( + emitterChainId vaa.ChainID, + emitterAddrStr string, + dailyLimit uint64, + bigTransactionSize uint64, +) error { gov.mutex.Lock() defer gov.mutex.Unlock() @@ -70,7 +75,12 @@ func (gov *ChainGovernor) setChainForTesting(emitterChainId vaa.ChainID, emitter return nil } -func (gov *ChainGovernor) setTokenForTesting(tokenChainID vaa.ChainID, tokenAddrStr string, symbol string, price float64) error { +func (gov *ChainGovernor) setTokenForTesting( + tokenChainID vaa.ChainID, + tokenAddrStr string, + symbol string, + price float64, +) error { gov.mutex.Lock() defer gov.mutex.Unlock() @@ -96,6 +106,10 @@ func (gov *ChainGovernor) setTokenForTesting(tokenChainID vaa.ChainID, tokenAddr return nil } +// getStatsForAllChains sums the number of transfers, value of all transfers, number of pending transfers, +// and the value of the pending transfers. +// Note that 'flow cancel transfers' are not included and therefore the values returned by this function may not +// match the Governor usage. func (gov *ChainGovernor) getStatsForAllChains() (numTrans int, valueTrans uint64, numPending int, valuePending uint64) { gov.mutex.Lock() defer gov.mutex.Unlock() @@ -103,7 +117,7 @@ func (gov *ChainGovernor) getStatsForAllChains() (numTrans int, valueTrans uint6 for _, ce := range gov.chains { numTrans += len(ce.transfers) for _, te := range ce.transfers { - valueTrans += te.Value + valueTrans += te.dbTransfer.Value } numPending += len(ce.pending) @@ -137,10 +151,10 @@ func TestTrimEmptyTransfers(t *testing.T) { now, err := time.Parse("Jan 2, 2006 at 3:04pm (MST)", "Jun 1, 2022 at 12:00pm (CST)") require.NoError(t, err) - var transfers []*db.Transfer + var transfers []transfer sum, updatedTransfers, err := gov.TrimAndSumValue(transfers, now) require.NoError(t, err) - assert.Equal(t, uint64(0), sum) + assert.Equal(t, int64(0), sum) assert.Equal(t, 0, len(updatedTransfers)) } @@ -153,16 +167,223 @@ func TestSumAllFromToday(t *testing.T) { now, err := time.Parse("Jan 2, 2006 at 3:04pm (MST)", "Jun 1, 2022 at 12:00pm (CST)") require.NoError(t, err) - var transfers []*db.Transfer + var transfers []transfer transferTime, err := time.Parse("Jan 2, 2006 at 3:04pm (MST)", "Jun 1, 2022 at 11:00am (CST)") require.NoError(t, err) - transfers = append(transfers, &db.Transfer{Value: 125000, Timestamp: transferTime}) + dbTransfer := &db.Transfer{Value: 125000, Timestamp: transferTime} + transfer, err := newTransferFromDbTransfer(dbTransfer) + require.NoError(t, err) + transfers = append(transfers, transfer) sum, updatedTransfers, err := gov.TrimAndSumValue(transfers, now.Add(-time.Hour*24)) require.NoError(t, err) - assert.Equal(t, uint64(125000), sum) + assert.Equal(t, uint64(125000), uint64(sum)) assert.Equal(t, 1, len(updatedTransfers)) } +func TestSumWithFlowCancelling(t *testing.T) { + ctx := context.Background() + gov, err := newChainGovernorForTest(ctx) + require.NoError(t, err) + assert.NotNil(t, gov) + + // Choose a hard-coded value from the Flow Cancel Token List + // NOTE: Replace this Chain:Address pair if the Flow Cancel Token List is modified + var originChain vaa.ChainID = 2 + var originAddress vaa.Address + originAddress, err = vaa.StringToAddress("000000000000000000000000bcca60bb61934080951369a648fb03df4f96263c") + require.NoError(t, err) + + // Ensure asset is registered in the governor and can flow cancel + key := tokenKey{originChain, originAddress} + assert.True(t, gov.tokens[key].flowCancels) + + now, err := time.Parse("2006-Jan-02", "2024-Feb-19") + require.NoError(t, err) + + var chainEntryTransfers []transfer + transferTime, err := time.Parse("2006-Jan-02", "2024-Feb-19") + require.NoError(t, err) + + // Set up values and governor limit + emitterTransferValue := uint64(125000) + flowCancelValue := uint64(100000) + + emitterLimit := emitterTransferValue * 2 // make sure the limit always exceeds the transfer value + emitterChainId := 1 + + // Setup transfers + // - Transfer from emitter: we only care about Value + // - Transfer that flow cancels: Transfer must be a valid entry from FlowCancelTokenList() (based on origin chain and origin address) + // and the destination chain must be the same as the emitter chain + outgoingDbTransfer := &db.Transfer{Value: emitterTransferValue, Timestamp: transferTime} + outgoingTransfer, err := newTransferFromDbTransfer(outgoingDbTransfer) + require.NoError(t, err) + + // Flow cancelling transfer + incomingDbTransfer := &db.Transfer{ + OriginChain: originChain, + OriginAddress: originAddress, + TargetChain: vaa.ChainID(emitterChainId), // emitter + Value: flowCancelValue, + Timestamp: transferTime, + } + + chainEntryTransfers = append(chainEntryTransfers, outgoingTransfer) + + // Populate chainEntry and ChainGovernor + emitter := &chainEntry{ + transfers: chainEntryTransfers, + emitterChainId: vaa.ChainID(emitterChainId), + dailyLimit: emitterLimit, + } + + err = emitter.addFlowCancelTransferFromDbTransfer(incomingDbTransfer) + require.NoError(t, err) + + gov.chains[emitter.emitterChainId] = emitter + + // Sanity check: ensure that there are transfers in the chainEntry + expectedNumTransfers := 2 + _, transfers, err := gov.TrimAndSumValue(emitter.transfers, now) + require.NoError(t, err) + assert.Equal(t, expectedNumTransfers, len(transfers)) + + // Calculate Governor Usage for emitter, including flow cancelling. + usage, err := gov.TrimAndSumValueForChain(emitter, now.Add(-time.Hour*24)) + require.NoError(t, err) + difference := uint64(25000) // emitterTransferValue - flowCancelTransferValue + assert.Equal(t, difference, usage) +} + +// Flow cancelling transfers are subtracted from the overall sum of all transfers from a given +// emitter chain. Since we are working with uint64 values, ensure that there is no underflow. +// When the sum of all flow cancelling transfers is greater than emitted transfers for a chain, +// the expected result is that the resulting Governor Usage equals 0 (and not a negative number +// or a very large underflow result). +// Also, the function should not return an error in this case. +func TestFlowCancelCannotUnderflow(t *testing.T) { + ctx := context.Background() + gov, err := newChainGovernorForTest(ctx) + require.NoError(t, err) + assert.NotNil(t, gov) + + // Set-up asset to be used in the test + // NOTE: Replace this Chain:Address pair if the Flow Cancel Token List is modified + var originChain vaa.ChainID = 2 + var originAddress vaa.Address + originAddress, err = vaa.StringToAddress("000000000000000000000000bcca60bb61934080951369a648fb03df4f96263c") + require.NoError(t, err) + + // Ensure asset is registered in the governor and can flow cancel + key := tokenKey{originChain, originAddress} + assert.True(t, gov.tokens[key].flowCancels) + + now, err := time.Parse("2006-Jan-02", "2024-Feb-19") + require.NoError(t, err) + + var transfers_from_emitter []transfer + transferTime, err := time.Parse("2006-Jan-02", "2024-Feb-19") + require.NoError(t, err) + + // Set up values and governor limit + emitterTransferValue := uint64(100000) + flowCancelValue := emitterTransferValue + 25000 // make sure this value is higher than `emitterTransferValue` + + emitterLimit := emitterTransferValue * 2 // make sure the limit always exceeds the transfer value + emitterChainId := 1 + + // Setup transfers + // - Transfer from emitter: we only care about Value + // - Transfer that flow cancels: Transfer must be a valid entry from FlowCancelTokenList() (based on origin chain and origin address) + // and the destination chain must be the same as the emitter chain + emitterDbTransfer := &db.Transfer{Value: emitterTransferValue, Timestamp: transferTime} + emitterTransfer, err := newTransferFromDbTransfer(emitterDbTransfer) + require.NoError(t, err) + transfers_from_emitter = append(transfers_from_emitter, emitterTransfer) + + flowCancelDbTransfer := &db.Transfer{ + OriginChain: originChain, + OriginAddress: originAddress, + TargetChain: vaa.ChainID(emitterChainId), // emitter + Value: flowCancelValue, + Timestamp: transferTime, + } + + // Populate chainEntrys and ChainGovernor + emitter := &chainEntry{ + transfers: transfers_from_emitter, + emitterChainId: vaa.ChainID(emitterChainId), + dailyLimit: emitterLimit, + } + err = emitter.addFlowCancelTransferFromDbTransfer(flowCancelDbTransfer) + require.NoError(t, err) + + gov.chains[emitter.emitterChainId] = emitter + + expectedNumTransfers := 2 + _, transfers, err := gov.TrimAndSumValue(emitter.transfers, now) + require.NoError(t, err) + assert.Equal(t, expectedNumTransfers, len(transfers)) + + // Calculate Governor Usage for emitter, including flow cancelling + // Should be zero when flow cancel transfer values exceed emitted transfer values. + usage, err := gov.TrimAndSumValueForChain(emitter, now.Add(-time.Hour*24)) + require.NoError(t, err) + assert.Zero(t, usage) +} + +// Simulate a case where the total sum of transfers for a chain in a 24 hour period exceeds +// the configured Governor limit. This should never happen, so we make sure that an error +// is returned if the system is in this state +func TestInvariantGovernorLimit(t *testing.T) { + ctx := context.Background() + gov, err := newChainGovernorForTest(ctx) + require.NoError(t, err) + assert.NotNil(t, gov) + + now, err := time.Parse("2006-Jan-02", "2024-Feb-19") + require.NoError(t, err) + + var transfers_from_emitter []transfer + transferTime, err := time.Parse("2006-Jan-02", "2024-Feb-19") + require.NoError(t, err) + + emitterTransferValue := uint64(125000) + + emitterLimit := emitterTransferValue * 20 + emitterChainId := 1 + + // Create a lot of transfers. Their total value should exceed `emitterLimit` + for i := 0; i < 25; i++ { + transfer, err := newTransferFromDbTransfer(&db.Transfer{Value: emitterTransferValue, Timestamp: transferTime}) + require.NoError(t, err) + transfers_from_emitter = append( + transfers_from_emitter, + transfer, + ) + } + + // Populate chainEntry and ChainGovernor + emitter := &chainEntry{ + transfers: transfers_from_emitter, + emitterChainId: vaa.ChainID(emitterChainId), + dailyLimit: emitterLimit, + } + gov.chains[emitter.emitterChainId] = emitter + + // XXX: sanity check + expectedNumTransfers := 25 + sum, transfers, err := gov.TrimAndSumValue(emitter.transfers, now) + require.NoError(t, err) + assert.Equal(t, expectedNumTransfers, len(transfers)) + assert.NotZero(t, sum) + + // Make sure we trigger the Invariant + usage, err := gov.TrimAndSumValueForChain(emitter, now.Add(-time.Hour*24)) + require.ErrorContains(t, err, "invariant violation: calculated sum") + assert.Zero(t, usage) +} + func TestTrimOneOfTwoTransfers(t *testing.T) { ctx := context.Background() gov, err := newChainGovernorForTest(ctx) @@ -172,23 +393,29 @@ func TestTrimOneOfTwoTransfers(t *testing.T) { now, err := time.Parse("Jan 2, 2006 at 3:04pm (MST)", "Jun 1, 2022 at 12:00pm (CST)") require.NoError(t, err) - var transfers []*db.Transfer + var transfers []transfer // The first transfer should be expired. transferTime1, err := time.Parse("Jan 2, 2006 at 3:04pm (MST)", "May 31, 2022 at 11:59am (CST)") require.NoError(t, err) - transfers = append(transfers, &db.Transfer{Value: 125000, Timestamp: transferTime1}) + dbTransfer := &db.Transfer{Value: 125000, Timestamp: transferTime1} + transfer, err := newTransferFromDbTransfer(dbTransfer) + require.NoError(t, err) + transfers = append(transfers, transfer) // But the second should not. transferTime2, err := time.Parse("Jan 2, 2006 at 3:04pm (MST)", "May 31, 2022 at 1:00pm (CST)") require.NoError(t, err) - transfers = append(transfers, &db.Transfer{Value: 225000, Timestamp: transferTime2}) + dbTransfer = &db.Transfer{Value: 225000, Timestamp: transferTime2} + transfer2, err := newTransferFromDbTransfer(dbTransfer) + require.NoError(t, err) + transfers = append(transfers, transfer2) assert.Equal(t, 2, len(transfers)) sum, updatedTransfers, err := gov.TrimAndSumValue(transfers, now.Add(-time.Hour*24)) require.NoError(t, err) assert.Equal(t, 1, len(updatedTransfers)) - assert.Equal(t, uint64(225000), sum) + assert.Equal(t, uint64(225000), uint64(sum)) } func TestTrimSeveralTransfers(t *testing.T) { @@ -200,36 +427,51 @@ func TestTrimSeveralTransfers(t *testing.T) { now, err := time.Parse("Jan 2, 2006 at 3:04pm (MST)", "Jun 1, 2022 at 12:00pm (CST)") require.NoError(t, err) - var transfers []*db.Transfer + var transfers []transfer // The first two transfers should be expired. transferTime1, err := time.Parse("Jan 2, 2006 at 3:04pm (MST)", "May 31, 2022 at 10:00am (CST)") require.NoError(t, err) - transfers = append(transfers, &db.Transfer{Value: 125000, Timestamp: transferTime1}) + dbTransfer1 := &db.Transfer{Value: 125000, Timestamp: transferTime1} + transfer1, err := newTransferFromDbTransfer(dbTransfer1) + require.NoError(t, err) + transfers = append(transfers, transfer1) transferTime2, err := time.Parse("Jan 2, 2006 at 3:04pm (MST)", "May 31, 2022 at 11:00am (CST)") require.NoError(t, err) - transfers = append(transfers, &db.Transfer{Value: 135000, Timestamp: transferTime2}) + dbTransfer2 := &db.Transfer{Value: 135000, Timestamp: transferTime2} + transfer2, err := newTransferFromDbTransfer(dbTransfer2) + require.NoError(t, err) + transfers = append(transfers, transfer2) // But the next three should not. transferTime3, err := time.Parse("Jan 2, 2006 at 3:04pm (MST)", "May 31, 2022 at 1:00pm (CST)") require.NoError(t, err) - transfers = append(transfers, &db.Transfer{Value: 145000, Timestamp: transferTime3}) + dbTransfer3 := &db.Transfer{Value: 145000, Timestamp: transferTime3} + transfer3, err := newTransferFromDbTransfer(dbTransfer3) + require.NoError(t, err) + transfers = append(transfers, transfer3) transferTime4, err := time.Parse("Jan 2, 2006 at 3:04pm (MST)", "May 31, 2022 at 2:00pm (CST)") require.NoError(t, err) - transfers = append(transfers, &db.Transfer{Value: 155000, Timestamp: transferTime4}) + dbTransfer4 := &db.Transfer{Value: 155000, Timestamp: transferTime4} + transfer4, err := newTransferFromDbTransfer(dbTransfer4) + require.NoError(t, err) + transfers = append(transfers, transfer4) transferTime5, err := time.Parse("Jan 2, 2006 at 3:04pm (MST)", "May 31, 2022 at 2:00pm (CST)") require.NoError(t, err) - transfers = append(transfers, &db.Transfer{Value: 165000, Timestamp: transferTime5}) + dbTransfer5 := &db.Transfer{Value: 165000, Timestamp: transferTime5} + transfer5, err := newTransferFromDbTransfer(dbTransfer5) + require.NoError(t, err) + transfers = append(transfers, transfer5) assert.Equal(t, 5, len(transfers)) sum, updatedTransfers, err := gov.TrimAndSumValue(transfers, now.Add(-time.Hour*24)) require.NoError(t, err) assert.Equal(t, 3, len(updatedTransfers)) - assert.Equal(t, uint64(465000), sum) + assert.Equal(t, uint64(465000), uint64(sum)) } func TestTrimmingAllTransfersShouldReturnZero(t *testing.T) { @@ -241,21 +483,28 @@ func TestTrimmingAllTransfersShouldReturnZero(t *testing.T) { now, err := time.Parse("Jan 2, 2006 at 3:04pm (MST)", "Jun 1, 2022 at 12:00pm (CST)") require.NoError(t, err) - var transfers []*db.Transfer + var transfers []transfer transferTime1, err := time.Parse("Jan 2, 2006 at 3:04pm (MST)", "May 31, 2022 at 11:00am (CST)") require.NoError(t, err) - transfers = append(transfers, &db.Transfer{Value: 125000, Timestamp: transferTime1}) + dbTransfer1 := &db.Transfer{Value: 125000, Timestamp: transferTime1} + transfer1, err := newTransferFromDbTransfer(dbTransfer1) + require.NoError(t, err) + transfers = append(transfers, transfer1) transferTime2, err := time.Parse("Jan 2, 2006 at 3:04pm (MST)", "May 31, 2022 at 11:45am (CST)") require.NoError(t, err) - transfers = append(transfers, &db.Transfer{Value: 225000, Timestamp: transferTime2}) + dbTransfer2 := &db.Transfer{Value: 125000, Timestamp: transferTime2} + transfer2, err := newTransferFromDbTransfer(dbTransfer2) + require.NoError(t, err) + transfers = append(transfers, transfer2) + assert.Equal(t, 2, len(transfers)) sum, updatedTransfers, err := gov.TrimAndSumValue(transfers, now) require.NoError(t, err) assert.Equal(t, 0, len(updatedTransfers)) - assert.Equal(t, uint64(0), sum) + assert.Equal(t, int64(0), sum) } func newChainGovernorForTest(ctx context.Context) (*ChainGovernor, error) { @@ -316,7 +565,7 @@ func TestVaaForUninterestingEmitterChain(t *testing.T) { assert.NotNil(t, gov) emitterAddr, _ := vaa.StringToAddress("0x00") - var payload = []byte{1, 97, 97, 97, 97, 97} + payload := []byte{1, 97, 97, 97, 97, 97} msg := common.MessagePublication{ TxHash: hashFromString("0x06f541f5ecfc43407c31587aa6ac3a689e8960f36dc23c332db5510dfc6a4063"), @@ -348,7 +597,7 @@ func TestVaaForUninterestingEmitterAddress(t *testing.T) { assert.NotNil(t, gov) emitterAddr, _ := vaa.StringToAddress("0x00") - var payload = []byte{1, 97, 97, 97, 97, 97} + payload := []byte{1, 97, 97, 97, 97, 97} msg := common.MessagePublication{ TxHash: hashFromString("0x06f541f5ecfc43407c31587aa6ac3a689e8960f36dc23c332db5510dfc6a4063"), @@ -381,7 +630,7 @@ func TestVaaForUninterestingPayloadType(t *testing.T) { assert.NotNil(t, gov) emitterAddr, _ := vaa.StringToAddress("0x0290fb167208af455bb137780163b7b7a9a10c16") - var payload = []byte{2, 97, 97, 97, 97, 97} + payload := []byte{2, 97, 97, 97, 97, 97} msg := common.MessagePublication{ TxHash: hashFromString("0x06f541f5ecfc43407c31587aa6ac3a689e8960f36dc23c332db5510dfc6a4063"), @@ -1267,7 +1516,8 @@ func TestLargeTransactionGetsEnqueuedAndReleasedWhenTheTimerExpires(t *testing.T // But the big transaction should not affect the daily notional. ce, exists := gov.chains[vaa.ChainIDEthereum] require.Equal(t, true, exists) - valueTrans = sumValue(ce.transfers, now) + valueTrans, err = sumValue(ce.transfers, now) + require.NoError(t, err) assert.Equal(t, uint64(0), valueTrans) } @@ -1527,7 +1777,8 @@ func TestDontReloadDuplicates(t *testing.T) { assert.Equal(t, 4, len(pendings)) for _, p := range xfers { - gov.reloadTransfer(p) + err := gov.reloadTransfer(p) + require.NoError(t, err) } for _, p := range pendings { @@ -1936,3 +2187,84 @@ func TestPendingTransferWithBadPayloadGetsDroppedNotReleased(t *testing.T) { _, exists = gov.msgsSeen[gov.HashFromMsg(&msg2)] assert.False(t, exists) } + +func TestCheckedAddUint64HappyPath(t *testing.T) { + // Both non-zero + x := uint64(1000) + y := uint64(337) + sum, err := CheckedAddUint64(x, y) + require.NoError(t, err) + assert.Equal(t, uint64(1337), sum) + + // x is zero + x = 0 + y = 2000 + sum, err = CheckedAddUint64(x, y) + require.NoError(t, err) + assert.Equal(t, uint64(2000), sum) + + // y is zero + x = 3000 + y = 0 + sum, err = CheckedAddUint64(x, y) + require.NoError(t, err) + assert.Equal(t, uint64(3000), sum) +} + +func TestCheckedAddInt64HappyPath(t *testing.T) { + // Two positive numbers + x := int64(1000) + y := int64(337) + sum, err := CheckedAddInt64(x, y) + require.NoError(t, err) + assert.Equal(t, int64(1337), sum) + + // One positive, one negative + x = 100 + y = -1000 + sum, err = CheckedAddInt64(x, y) + require.NoError(t, err) + assert.Equal(t, int64(-900), sum) + + // Both negative + x = -100 + y = -1000 + sum, err = CheckedAddInt64(x, y) + require.NoError(t, err) + assert.Equal(t, int64(-1100), sum) + + // x is zero + x = 0 + y = 2000 + sum, err = CheckedAddInt64(x, y) + require.NoError(t, err) + assert.Equal(t, int64(2000), sum) + + // y is zero + x = 3000 + y = 0 + sum, err = CheckedAddInt64(x, y) + require.NoError(t, err) + assert.Equal(t, int64(3000), sum) +} + +func TestCheckedAddUint64ReturnsErrorOnOverflow(t *testing.T) { + // Return error on overflow + sum, err := CheckedAddUint64(math.MaxUint64, 1) + require.Error(t, err) + assert.Equal(t, uint64(0), sum) +} + +func TestCheckedAddInt64ReturnsErrorOnOverflow(t *testing.T) { + // Return error on overflow + sum, err := CheckedAddInt64(math.MaxInt64, 1) + require.Error(t, err) + assert.Equal(t, int64(0), sum) +} + +func TestCheckedAddInt64ReturnsErrorOnUnderflow(t *testing.T) { + // Return error on underflow + sum, err := CheckedAddInt64(math.MinInt64, -1) + require.Error(t, err) + assert.Equal(t, int64(0), sum) +} diff --git a/node/pkg/governor/testnet_config.go b/node/pkg/governor/testnet_config.go index ded45f7458..c2650bcc17 100644 --- a/node/pkg/governor/testnet_config.go +++ b/node/pkg/governor/testnet_config.go @@ -6,18 +6,23 @@ import ( "github.com/wormhole-foundation/wormhole/sdk/vaa" ) -func (gov *ChainGovernor) initTestnetConfig() ([]tokenConfigEntry, []chainConfigEntry) { +func (gov *ChainGovernor) initTestnetConfig() ([]tokenConfigEntry, []tokenConfigEntry, []chainConfigEntry) { gov.logger.Info("setting up testnet config") tokens := []tokenConfigEntry{ - tokenConfigEntry{chain: 1, addr: "069b8857feab8184fb687f634618c035dac439dc1aeb3b5598a0f00000000001", symbol: "SOL", coinGeckoId: "wrapped-solana", decimals: 8, price: 34.94}, // Addr: So11111111111111111111111111111111111111112, Notional: 4145006 + {chain: 1, addr: "069b8857feab8184fb687f634618c035dac439dc1aeb3b5598a0f00000000001", symbol: "SOL", coinGeckoId: "wrapped-solana", decimals: 8, price: 34.94}, // Addr: So11111111111111111111111111111111111111112, Notional: 4145006tese + {chain: 1, addr: "3b442cb3912157f13a933d0134282d032b5ffecd01a2dbf1b7790608df002ea7", symbol: "USDC", coinGeckoId: "usdc", decimals: 6, price: 1}, // Addr: 4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU, Notional: 1 + } + + flowCancelTokens := []tokenConfigEntry{ + {chain: 1, addr: "3b442cb3912157f13a933d0134282d032b5ffecd01a2dbf1b7790608df002ea7", symbol: "USDC", coinGeckoId: "usdc", decimals: 6, price: 1}, // Addr: 4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU, Notional: 1 } chains := []chainConfigEntry{ - chainConfigEntry{emitterChainID: vaa.ChainIDSolana, dailyLimit: 100000000}, - chainConfigEntry{emitterChainID: vaa.ChainIDEthereum, dailyLimit: 100000000}, - chainConfigEntry{emitterChainID: vaa.ChainIDFantom, dailyLimit: 1000000}, + {emitterChainID: vaa.ChainIDSolana, dailyLimit: 100000000}, + {emitterChainID: vaa.ChainIDEthereum, dailyLimit: 100000000}, + {emitterChainID: vaa.ChainIDFantom, dailyLimit: 1000000}, } - return tokens, chains + return tokens, flowCancelTokens, chains }