diff --git a/services/wallet/transfer/commands.go b/services/wallet/transfer/commands.go index 50339f486..5af5c1596 100644 --- a/services/wallet/transfer/commands.go +++ b/services/wallet/transfer/commands.go @@ -442,8 +442,8 @@ func (c *transfersCommand) checkAndProcessPendingMultiTx(subTx *Transfer) (Multi func (c *transfersCommand) checkAndProcessSwapMultiTx(ctx context.Context, subTx *Transfer) (MultiTransactionIDType, error) { switch subTx.Type { - // If the Tx contains any uniswapV2Swap subTx, generate a Swap multiTx - case uniswapV2Swap: + // If the Tx contains any uniswapV2Swap/uniswapV3Swap subTx, generate a Swap multiTx + case uniswapV2Swap, uniswapV3Swap: multiTransaction, err := buildUniswapSwapMultitransaction(ctx, c.chainClient, c.tokenManager, subTx) if err != nil { return NoMultiTransactionID, err diff --git a/services/wallet/transfer/downloader.go b/services/wallet/transfer/downloader.go index ff63936e0..d56bc096a 100644 --- a/services/wallet/transfer/downloader.go +++ b/services/wallet/transfer/downloader.go @@ -306,7 +306,7 @@ func (d *ETHDownloader) subTransactionsFromTransactionHash(parent context.Contex if from == address || to == address { mustAppend = true } - case uniswapV2SwapEventType: + case uniswapV2SwapEventType, uniswapV3SwapEventType: mustAppend = true } diff --git a/services/wallet/transfer/log_parser.go b/services/wallet/transfer/log_parser.go index 557d1f974..965ea55fb 100644 --- a/services/wallet/transfer/log_parser.go +++ b/services/wallet/transfer/log_parser.go @@ -21,12 +21,14 @@ const ( erc20Transfer Type = "erc20" erc721Transfer Type = "erc721" uniswapV2Swap Type = "uniswapV2Swap" + uniswapV3Swap Type = "uniswapV3Swap" unknownTransaction Type = "unknown" // Event types erc20TransferEventType EventType = "erc20Event" erc721TransferEventType EventType = "erc721Event" uniswapV2SwapEventType EventType = "uniswapV2SwapEvent" + uniswapV3SwapEventType EventType = "uniswapV3SwapEvent" unknownEventType EventType = "unknownEvent" erc20_721TransferEventSignature = "Transfer(address,address,uint256)" @@ -35,6 +37,7 @@ const ( erc721TransferEventIndexedParameters = 4 // signature, from, to, tokenId uniswapV2SwapEventSignature = "Swap(address,uint256,uint256,uint256,uint256,address)" // also used by SushiSwap + uniswapV3SwapEventSignature = "Swap(address,address,int256,int256,uint160,uint128,int24)" ) var ( @@ -46,6 +49,7 @@ var ( func GetEventType(log *types.Log) EventType { erc20_721TransferEventSignatureHash := getEventSignatureHash(erc20_721TransferEventSignature) uniswapV2SwapEventSignatureHash := getEventSignatureHash(uniswapV2SwapEventSignature) + uniswapV3SwapEventSignatureHash := getEventSignatureHash(uniswapV3SwapEventSignature) if len(log.Topics) > 0 { switch log.Topics[0] { @@ -58,6 +62,8 @@ func GetEventType(log *types.Log) EventType { } case uniswapV2SwapEventSignatureHash: return uniswapV2SwapEventType + case uniswapV3SwapEventSignatureHash: + return uniswapV3SwapEventType } } @@ -72,6 +78,8 @@ func EventTypeToSubtransactionType(eventType EventType) Type { return erc721Transfer case uniswapV2SwapEventType: return uniswapV2Swap + case uniswapV3SwapEventType: + return uniswapV3Swap } return unknownTransaction @@ -177,3 +185,47 @@ func parseUniswapV2Log(ethlog *types.Log) (pairAddress common.Address, from comm return } + +func readInt256(b []byte) *big.Int { + // big.SetBytes can't tell if a number is negative or positive in itself. + // On EVM, if the returned number > max int256, it is negative. + // A number is > max int256 if the bit at position 255 is set. + ret := new(big.Int).SetBytes(b) + if ret.Bit(255) == 1 { + ret.Add(MaxUint256, new(big.Int).Neg(ret)) + ret.Add(ret, common.Big1) + ret.Neg(ret) + } + return ret +} + +func parseUniswapV3Log(ethlog *types.Log) (poolAddress common.Address, sender common.Address, recipient common.Address, amount0 *big.Int, amount1 *big.Int, err error) { + amount0 = new(big.Int) + amount1 = new(big.Int) + + if len(ethlog.Topics) < 3 { + err = fmt.Errorf("not enough topics for uniswapV3 swap %s, %v", "topics", ethlog.Topics) + return + } + + poolAddress = ethlog.Address + + if len(ethlog.Topics[1]) != 32 { + err = fmt.Errorf("second topic is not padded to 32 byte address %s, %v", "topic", ethlog.Topics[1]) + return + } + if len(ethlog.Topics[2]) != 32 { + err = fmt.Errorf("third topic is not padded to 32 byte address %s, %v", "topic", ethlog.Topics[2]) + return + } + copy(sender[:], ethlog.Topics[1][12:]) + copy(recipient[:], ethlog.Topics[2][12:]) + if len(ethlog.Data) != 32*5 { + err = fmt.Errorf("data is not padded to 5 * 32 bytes big int %s, %v", "data", ethlog.Data) + return + } + amount0 = readInt256(ethlog.Data[0:32]) + amount1 = readInt256(ethlog.Data[32:64]) + + return +} diff --git a/services/wallet/transfer/swap_identifier.go b/services/wallet/transfer/swap_identifier.go index 7c9a134bd..70720caff 100644 --- a/services/wallet/transfer/swap_identifier.go +++ b/services/wallet/transfer/swap_identifier.go @@ -38,6 +38,29 @@ func fetchUniswapV2PairInfo(ctx context.Context, client *chain.ClientWithFallbac return &token0Address, &token1Address, nil } +func fetchUniswapV3PoolInfo(ctx context.Context, client *chain.ClientWithFallback, poolAddress common.Address) (*common.Address, *common.Address, error) { + caller, err := uniswapv3.NewUniswapv3Caller(poolAddress, client) + if err != nil { + return nil, nil, err + } + + token0Address, err := caller.Token0(&bind.CallOpts{ + Context: ctx, + }) + if err != nil { + return nil, nil, err + } + + token1Address, err := caller.Token1(&bind.CallOpts{ + Context: ctx, + }) + if err != nil { + return nil, nil, err + } + + return &token0Address, &token1Address, nil +} + func identifyUniswapV2Asset(tokenManager *token.Manager, chainID uint64, amount0 *big.Int, contractAddress0 common.Address, amount1 *big.Int, contractAddress1 common.Address) (token *token.Token, amount *big.Int, err error) { // Either amount0 or amount1 should be 0 if amount1.Sign() == 0 && amount0.Sign() != 0 { @@ -97,16 +120,81 @@ func fetchUniswapV2Info(ctx context.Context, client *chain.ClientWithFallback, t return } +func identifyUniswapV3Assets(tokenManager *token.Manager, chainID uint64, amount0 *big.Int, contractAddress0 common.Address, amount1 *big.Int, contractAddress1 common.Address) (fromToken *token.Token, fromAmount *big.Int, toToken *token.Token, toAmount *big.Int, err error) { + token0 := tokenManager.FindTokenByAddress(chainID, contractAddress0) + if token0 == nil { + err = fmt.Errorf("couldn't find symbol for token0 %v", contractAddress0) + return + } + + token1 := tokenManager.FindTokenByAddress(chainID, contractAddress1) + if token1 == nil { + err = fmt.Errorf("couldn't find symbol for token1 %v", contractAddress1) + return + } + + // amount0 and amount1 are the balance deltas of the pool + // The positive amount is how much the sender spent + // The negative amount is how much the recipent got + if amount0.Sign() > 0 && amount1.Sign() < 0 { + fromToken = token0 + fromAmount = amount0 + toToken = token1 + toAmount = new(big.Int).Neg(amount1) + } else if amount0.Sign() < 0 && amount1.Sign() > 0 { + fromToken = token1 + fromAmount = amount1 + toToken = token0 + toAmount = new(big.Int).Neg(amount0) + } else { + err = fmt.Errorf("couldn't identify tokens %v %v %v %v", contractAddress0, amount0, contractAddress1, amount1) + return + } + + return +} + +func fetchUniswapV3Info(ctx context.Context, client *chain.ClientWithFallback, tokenManager *token.Manager, log *types.Log) (fromAsset string, fromAmount *hexutil.Big, toAsset string, toAmount *hexutil.Big, err error) { + poolAddress, _, _, amount0, amount1, err := parseUniswapV3Log(log) + if err != nil { + return + } + + token0ContractAddress, token1ContractAddress, err := fetchUniswapV3PoolInfo(ctx, client, poolAddress) + if err != nil { + return + } + + fromToken, fromAmountInt, toToken, toAmountInt, err := identifyUniswapV3Assets(tokenManager, client.ChainID, amount0, *token0ContractAddress, amount1, *token1ContractAddress) + if err != nil { + // "Soft" error, allow to continue with unknown asset + err = nil + fromAsset = "" + fromAmount = (*hexutil.Big)(big.NewInt(0)) + toAsset = "" + toAmount = (*hexutil.Big)(big.NewInt(0)) + } else { + fromAsset = fromToken.Symbol + fromAmount = (*hexutil.Big)(fromAmountInt) + toAsset = toToken.Symbol + toAmount = (*hexutil.Big)(toAmountInt) + } + + return +} + func fetchUniswapInfo(ctx context.Context, client *chain.ClientWithFallback, tokenManager *token.Manager, log *types.Log, logType EventType) (fromAsset string, fromAmount *hexutil.Big, toAsset string, toAmount *hexutil.Big, err error) { switch logType { case uniswapV2SwapEventType: return fetchUniswapV2Info(ctx, client, tokenManager, log) + case uniswapV3SwapEventType: + return fetchUniswapV3Info(ctx, client, tokenManager, log) } err = fmt.Errorf("wrong log type %s", logType) return } -// Build a Swap multitransaction from a list containing one or several uniswapV2 subTxs +// Build a Swap multitransaction from a list containing one or several uniswapV2/uniswapV3 subTxs // We only care about the first and last swap to identify the input/output token and amounts func buildUniswapSwapMultitransaction(ctx context.Context, client *chain.ClientWithFallback, tokenManager *token.Manager, transfer *Transfer) (*MultiTransaction, error) { multiTransaction := MultiTransaction{ @@ -121,7 +209,7 @@ func buildUniswapSwapMultitransaction(ctx context.Context, client *chain.ClientW for _, ethlog := range transfer.Receipt.Logs { logType := GetEventType(ethlog) switch logType { - case uniswapV2SwapEventType: + case uniswapV2SwapEventType, uniswapV3SwapEventType: if firstSwapLog == nil { firstSwapLog = ethlog firstSwapLogType = logType