├── .gitignore ├── LICENSE ├── README.md ├── actions ├── action_bytecode_scan.go ├── action_deployment_scan.go ├── action_erc20.go ├── action_graph.go ├── action_initscan.go ├── action_logging.go ├── action_monitor_transfers.go ├── action_ownershiptransfer.go ├── action_proxy_upgrades.go ├── action_send_transaction.go ├── action_tethertrack.go ├── action_tornado_funded_exploits.go ├── action_tornadocash.go ├── action_track_contract_calls.go ├── action_uniswapV2_volume.go ├── action_uniswapV3_volume.go ├── base.go └── init.go ├── contracts ├── IERC20 │ ├── IERC20.go │ ├── IERC20.sol │ └── abi │ │ └── IERC20.abi ├── IERC20Metadata │ ├── IERC20Metadata.go │ ├── IERC20Metadata.sol │ └── abi │ │ ├── IERC20.abi │ │ └── IERC20Metadata.abi ├── Multicall2 │ ├── Multicall2.go │ ├── Multicall2.sol │ ├── Multicall2_sol_Multicall2.abi │ ├── Multicall2_sol_Multicall2.bin │ └── Multicall2_test.go ├── UniswapV3Factory │ ├── UniswapV3Factory.go │ ├── code.abi │ └── code.bin └── uniswapPool │ └── UniswapV3Pool.go ├── data ├── blockscanners.json ├── event_sigs.json └── func_sigs.json ├── database ├── eventdata.go └── funcdata.go ├── discord-webhook └── discordwebhook.go ├── docs └── imgs │ ├── banner.png │ ├── discord_tornado.png │ ├── discord_uniswap.png │ ├── graph.png │ ├── logging.png │ └── ownership_transfer_proxy_upgrade_example.png ├── example_config.json ├── go.mod ├── go.sum ├── main.go ├── shared ├── common.go ├── constants.go ├── decoding.go ├── helpers.go ├── l2helpers.go ├── tracing.go └── webhook.go ├── trackooor ├── base.go ├── blocklistener.go ├── eventlistener.go ├── historical.go └── logging.go └── utils ├── formatting.go ├── math.go └── utils.go /.gitignore: -------------------------------------------------------------------------------- 1 | # If you prefer the allow list template instead of the deny list, see community template: 2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 3 | # 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, built with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Dependency directories (remove the comment below to include it) 18 | # vendor/ 19 | 20 | # Go workspace file 21 | go.work 22 | 23 | # config files 24 | /config*.json 25 | 26 | # action data files 27 | /*_data.json 28 | 29 | # test files 30 | /graph.png 31 | /test.abi 32 | /test.json 33 | 34 | .DS_Store 35 | -------------------------------------------------------------------------------- /actions/action_bytecode_scan.go: -------------------------------------------------------------------------------- 1 | package actions 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "os" 8 | "sync" 9 | 10 | "github.com/Zellic/EVM-trackooor/shared" 11 | 12 | "github.com/ethereum/go-ethereum/common" 13 | ) 14 | 15 | var targetBytecodes [][]byte 16 | var rateLimitMutex sync.RWMutex 17 | 18 | func (p action) InitTesting() { 19 | 20 | if v, ok := p.o.CustomOptions["bytecodes"]; ok { 21 | hexBytecodeInterfaces := v.([]interface{}) 22 | for _, hexBytecodeInterface := range hexBytecodeInterfaces { 23 | hexBytecode := hexBytecodeInterface.(string) 24 | bytecode := common.Hex2Bytes(hexBytecode) 25 | targetBytecodes = append(targetBytecodes, bytecode) 26 | } 27 | } 28 | 29 | fmt.Printf("Looking for bytecodes:\n") 30 | for _, bytecode := range targetBytecodes { 31 | fmt.Printf("%v\n", common.Bytes2Hex(bytecode)) 32 | } 33 | 34 | addBlockAction(blockMined) 35 | } 36 | 37 | func blockMined(p ActionBlockData) { 38 | block := p.Block 39 | for _, tx := range block.Transactions() { 40 | // To() returns nil if tx is deployment tx 41 | if tx.To() == nil { 42 | // to := tx.To() 43 | // from := utils.GetTxSender(tx) 44 | 45 | deployedContract, _ := shared.GetDeployedContractAddress(tx) 46 | contractCode, err := shared.Client.CodeAt(context.Background(), deployedContract, block.Number()) 47 | if err != nil { 48 | panic(err) 49 | } 50 | 51 | // fmt.Printf("contract deployed\n") 52 | 53 | for _, bytecode := range targetBytecodes { 54 | if bytes.Contains(contractCode, bytecode) { 55 | fmt.Printf("Contract %v contains bytecode %v\n", deployedContract, bytecode) 56 | 57 | err = os.WriteFile("./output_test.out", []byte(fmt.Sprintf( 58 | "Contract %v contains bytecode %v\n", deployedContract, bytecode, 59 | )), 0644) 60 | if err != nil { 61 | panic(err) 62 | } 63 | } 64 | } 65 | 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /actions/action_erc20.go: -------------------------------------------------------------------------------- 1 | package actions 2 | 3 | import ( 4 | "fmt" 5 | "log/slog" 6 | "math/big" 7 | "strconv" 8 | "strings" 9 | "sync" 10 | "time" 11 | 12 | discordwebhook "github.com/Zellic/EVM-trackooor/discord-webhook" 13 | "github.com/Zellic/EVM-trackooor/shared" 14 | "github.com/Zellic/EVM-trackooor/utils" 15 | 16 | "github.com/ethereum/go-ethereum/common" 17 | "github.com/ethereum/go-ethereum/core/types" 18 | ) 19 | 20 | // tracking volume of ERC20 token, for last X minutes 21 | const erc20VolumeTime = 5 * 60 22 | 23 | // how often the webhook logs erc20 token volume 24 | const logERC20VolumeFreq = 1 * 60 25 | 26 | var erc20Volume map[common.Address]*big.Int 27 | var erc20Transfers map[common.Address][]ERC20TransferInfo 28 | var erc20Mutex = sync.RWMutex{} // prevent race condition for maps erc20Volume and erc20Transfers 29 | 30 | type ERC20TransferInfo struct { 31 | from common.Address 32 | to common.Address 33 | value *big.Int 34 | decimals uint8 35 | symbol string 36 | timestamp uint64 37 | } 38 | 39 | // init function called just before starting trackooor 40 | func (action) InitERC20Events() { 41 | erc20Volume = make(map[common.Address]*big.Int) 42 | erc20Transfers = make(map[common.Address][]ERC20TransferInfo) 43 | 44 | // log the volume of erc20 tokens every X minutes 45 | go func() { 46 | periodicLogERC20Volumes(shared.Options.FilterAddresses) 47 | }() 48 | 49 | // retrieve info for each ERC20 token 50 | shared.ERC20TokenInfosMutex.Lock() 51 | for _, contractAddress := range shared.Options.FilterAddresses { 52 | shared.ERC20TokenInfos[contractAddress] = shared.RetrieveERC20Info(contractAddress) 53 | } 54 | shared.ERC20TokenInfosMutex.Unlock() 55 | 56 | addEventSigAction( 57 | "Transfer(address,address,uint256)", 58 | ProcessERC20Transfer, 59 | ) 60 | } 61 | 62 | func SendWebhookMsgERC20Transfer(eventLog types.Log, erc20 ERC20TransferInfo) { 63 | var discordEmbedMsg discordwebhook.Embed 64 | discordEmbedMsg.Title = ":money_with_wings: ERC20 Transfer :money_with_wings:" 65 | discordEmbedAuthor := discordwebhook.Author{ 66 | Name: eventLog.Address.Hex(), 67 | } 68 | fromUrlStr := utils.BlockscanFormat("address", eventLog.Address.Hex()) 69 | // if we managed to format the URL (otherwise embed will fail to send) 70 | if strings.HasPrefix(fromUrlStr, "http") { 71 | discordEmbedAuthor.Url = fromUrlStr 72 | } 73 | discordEmbedMsg.Author = discordEmbedAuthor 74 | TxUrlStr := utils.BlockscanFormat("transaction", eventLog.TxHash.Hex()) 75 | if strings.HasPrefix(TxUrlStr, "http") { 76 | discordEmbedMsg.Url = TxUrlStr 77 | } 78 | discordEmbedMsg.Description = fmt.Sprintf( 79 | "from: %v\n"+ 80 | "to: %v\n"+ 81 | "value: %v (%v) %v (%v decimals)\n", 82 | utils.FormatBlockscanHyperlink("address", erc20.from.Hex(), erc20.from.Hex()), 83 | utils.FormatBlockscanHyperlink("address", erc20.to.Hex(), erc20.to.Hex()), 84 | utils.CodeQuote(erc20.value.String()), 85 | utils.CodeQuote(utils.FormatDecimals(erc20.value, erc20.decimals)), 86 | erc20.symbol, 87 | erc20.decimals, 88 | ) 89 | discordEmbedMsg.Timestamp = time.Now() 90 | 91 | // unique colour based on event signature hash 92 | eventSigHash := eventLog.Topics[0] 93 | colourHex := eventSigHash.Hex()[2 : 2+6] 94 | colourNum, _ := strconv.ParseUint(colourHex, 16, 64) 95 | discordEmbedMsg.Color = int(colourNum) 96 | 97 | err := shared.DiscordWebhook.SendEmbedMessages([]discordwebhook.Embed{discordEmbedMsg}) 98 | if err != nil { 99 | shared.Warnf(slog.Default(), "Webhook error: %v", err) 100 | } 101 | } 102 | 103 | func SendWebhookMsgERC20Volume(erc20Token common.Address, minutes int) { 104 | tokenInfo := shared.RetrieveERC20Info(erc20Token) 105 | 106 | erc20Mutex.RLock() 107 | amount := erc20Volume[erc20Token] 108 | erc20Mutex.RUnlock() 109 | 110 | shared.DiscordWebhook.SendMessage( 111 | fmt.Sprintf( 112 | "%v Volume in last %v mins\n"+ 113 | "%v (%v) %v\n", 114 | tokenInfo.Name, 115 | minutes/60, 116 | utils.CodeQuote(amount.String()), 117 | utils.CodeQuote(utils.FormatDecimals(amount, tokenInfo.Decimals)), 118 | tokenInfo.Symbol, 119 | ), 120 | ) 121 | } 122 | 123 | func periodicLogERC20Volumes(tokens []common.Address) { 124 | ticker := time.NewTicker(logERC20VolumeFreq * time.Second) 125 | for range ticker.C { 126 | shared.DiscordWebhook.SendMessage(":dollar: ERC20 Volumes :dollar:") 127 | for _, token := range tokens { 128 | SendWebhookMsgERC20Volume(token, erc20VolumeTime) 129 | } 130 | } 131 | } 132 | 133 | func updateERC20Volume(token common.Address) { 134 | // calculate erc20 volume in last X minutes 135 | volume := big.NewInt(0) 136 | currentTimestamp := time.Now().Unix() 137 | var removing []int 138 | 139 | erc20Mutex.RLock() 140 | for ind, transfer := range erc20Transfers[token] { 141 | // mark for removal if not during last X mins 142 | if uint64(currentTimestamp)-transfer.timestamp > erc20VolumeTime { 143 | removing = append(removing, ind) 144 | continue 145 | } 146 | volume.Add(volume, transfer.value) 147 | } 148 | erc20Mutex.RUnlock() 149 | 150 | // removing old data, looping backwards to avoid issues with indicies 151 | for i := len(removing) - 1; i >= 0; i-- { 152 | erc20Mutex.Lock() 153 | erc20Transfers[token] = append(erc20Transfers[token][:i], erc20Transfers[token][i+1:]...) 154 | erc20Mutex.Unlock() 155 | } 156 | 157 | // set volume 158 | erc20Mutex.Lock() 159 | erc20Volume[token] = volume 160 | erc20Mutex.Unlock() 161 | } 162 | 163 | func ProcessERC20Transfer(p ActionEventData) { 164 | // validate decoded event have fields we're expecting 165 | if _, ok := p.DecodedTopics["from"]; !ok { 166 | return 167 | } 168 | if _, ok := p.DecodedTopics["to"]; !ok { 169 | return 170 | } 171 | if _, ok := p.DecodedData["value"]; !ok { 172 | return 173 | } 174 | 175 | from := p.DecodedTopics["from"].(common.Address) 176 | to := p.DecodedTopics["to"].(common.Address) 177 | value := p.DecodedData["value"].(*big.Int) 178 | 179 | tokenAddress := p.EventLog.Address 180 | tokenInfo := shared.RetrieveERC20Info(tokenAddress) 181 | 182 | blockNum := p.EventLog.BlockNumber 183 | block := shared.GetBlockHeader(big.NewInt(int64(blockNum))) 184 | timestamp := block.Time 185 | 186 | erc20 := ERC20TransferInfo{ 187 | from: from, 188 | to: to, 189 | value: value, 190 | decimals: tokenInfo.Decimals, 191 | symbol: tokenInfo.Symbol, 192 | timestamp: timestamp, 193 | } 194 | 195 | erc20Mutex.Lock() 196 | erc20Transfers[tokenAddress] = append(erc20Transfers[tokenAddress], erc20) 197 | erc20Mutex.Unlock() 198 | 199 | // send discord embed msg 200 | // SendWebhookMsgERC20Transfer(p.EventLog, erc20) 201 | 202 | // update token volume 203 | updateERC20Volume(tokenAddress) 204 | } 205 | 206 | // helper functions 207 | -------------------------------------------------------------------------------- /actions/action_logging.go: -------------------------------------------------------------------------------- 1 | package actions 2 | 3 | import ( 4 | "encoding/hex" 5 | "fmt" 6 | "log" 7 | "log/slog" 8 | "strconv" 9 | "strings" 10 | "time" 11 | 12 | "github.com/Zellic/EVM-trackooor/utils" 13 | 14 | discordwebhook "github.com/Zellic/EVM-trackooor/discord-webhook" 15 | 16 | "github.com/Zellic/EVM-trackooor/shared" 17 | 18 | "github.com/ethereum/go-ethereum/core/types" 19 | ) 20 | 21 | // buffer for discord message to prevent rate limit 22 | // events emitted will pile up in this buffer until it reaches 23 | // length of 10, then it will send the whole buffer 24 | // max 10 embeds, may start dropping events after limit reached 25 | var embedBuffer []discordwebhook.Embed 26 | 27 | var logToTerminal bool 28 | var logToDiscord bool 29 | 30 | var logEvents bool 31 | var logTxs bool 32 | var logBlocks bool 33 | 34 | // logs deployment of any contract, not only contracts deployed by addresses we are tracking 35 | var logAnyDeployments bool 36 | 37 | // whether or not we want to know if tx was contract or EOA 38 | // as it takes 1 RPC request per tx to do so 39 | var determineTxType bool 40 | 41 | var bufferDiscordMessages bool 42 | 43 | func (actionInfo) InfoLogging() actionInfo { 44 | name := "Logging" 45 | overview := "Simply logs events/transactions from specified addresses, to terminal/Discord webhook. " + 46 | "Also contains option to log newly mined blocks." 47 | 48 | description := `This is useful for tracking activities done by specific addresses, or event signatures, and logging them. 49 | Note that event ABI must be added using evm trackooor data command for it to be logged.` 50 | 51 | options := `"log-events" - bool, whether to log events from the specified addresses 52 | "log-transactions" - bool, whether to log txs (including deployment txs) from the specified addresses 53 | "log-blocks" - bool, whether to log newly mined blocks 54 | "log-any-deployments" - bool, whether to log any deployments regardless of deployer address 55 | "determine-tx-type" - bool, whether to determine the tx type (EOA to EOA, EOA to contract), requires additional RPC call, don't enable if lots of txs 56 | "enable-terminal-logs" - bool, whether to log to the terminal 57 | "enable-discord-logs" - bool, whether to log to Discord webhook 58 | "discord-log-options" - dict, options for Discord webook logs 59 | "webhook-url" - Discord webook URL 60 | "username" - Discord webhook username (optional) 61 | "avatar-url" - Discord webhook avatar (optional) 62 | "buffer-webhook-messages" - bool, whether to buffer msgs (use if lots of event logs) 63 | "retry-webhook-messages" - bool, whether to retry failed msgs such as due to rate limit` 64 | 65 | example := `"Logging":{ 66 | "addresses":{ 67 | "0xdAC17F958D2ee523a2206206994597C13D831ec7":{} 68 | }, 69 | "options":{ 70 | "log-events": true, 71 | "log-transactions": true, 72 | "log-blocks": true, 73 | "log-any-deployments": false, 74 | "determine-tx-type": true, 75 | "enable-terminal-logs": true, 76 | "enable-discord-logs": false, 77 | "discord-log-options": { 78 | "webhook-url": "...", 79 | "username": "evm trackooor", 80 | "avatar-url": "...", 81 | "buffer-webhook-messages": true, 82 | "retry-webhook-messages": false 83 | } 84 | }, 85 | "enabled":true 86 | }` 87 | 88 | return actionInfo{ 89 | ActionName: name, 90 | ActionOverview: overview, 91 | ActionDescription: description, 92 | ActionOptionDetails: options, 93 | ActionConfigExample: example, 94 | } 95 | } 96 | 97 | func (p action) InitLogging() { 98 | 99 | if v, ok := p.o.CustomOptions["enable-terminal-logs"]; ok { 100 | logToTerminal = v.(bool) 101 | } 102 | if v, ok := p.o.CustomOptions["enable-discord-logs"]; ok { 103 | logToDiscord = v.(bool) 104 | // webhook settings 105 | if v, ok := p.o.CustomOptions["discord-log-options"]; ok { 106 | mapInterface := v.(map[string]interface{}) 107 | setDiscordWebhookOptions(mapInterface) 108 | } else { 109 | log.Fatalf("Discord logs enabled \"discord-log-options\" was not found!") 110 | } 111 | } 112 | if v, ok := p.o.CustomOptions["log-events"]; ok { 113 | logEvents = v.(bool) 114 | } 115 | if v, ok := p.o.CustomOptions["log-transactions"]; ok { 116 | logTxs = v.(bool) 117 | } 118 | if v, ok := p.o.CustomOptions["log-blocks"]; ok { 119 | logBlocks = v.(bool) 120 | } 121 | if v, ok := p.o.CustomOptions["determine-tx-type"]; ok { 122 | determineTxType = v.(bool) 123 | } 124 | if v, ok := p.o.CustomOptions["log-any-deployments"]; ok { 125 | logAnyDeployments = v.(bool) 126 | } 127 | 128 | if logToTerminal || logToDiscord { 129 | if logEvents { 130 | // a separate map for events map address -> action will be better? 131 | for _, address := range p.o.Addresses { 132 | addAddressEventAction(address, logEvent) 133 | } 134 | } 135 | if logTxs { 136 | for _, address := range p.o.Addresses { 137 | addTxAddressAction(address, logTransaction) 138 | } 139 | } 140 | if logBlocks { 141 | addBlockAction(logBlockMined) 142 | } 143 | if logAnyDeployments { // we will need to get all txs in a block and check if tx is a deployment tx 144 | addBlockAction(logDeployments) 145 | } 146 | } 147 | } 148 | 149 | func setDiscordWebhookOptions(optionsMap map[string]interface{}) { 150 | // webhook url must be set 151 | if v, ok := optionsMap["webhook-url"]; ok { 152 | shared.DiscordWebhook.WebhookURL = v.(string) 153 | } else { 154 | log.Fatalf("Events logging: discord \"webhook-url\" not provided in \"discord-log-options\"!") 155 | return 156 | } 157 | if v, ok := optionsMap["username"]; ok { 158 | shared.DiscordWebhook.Username = v.(string) 159 | } 160 | if v, ok := optionsMap["avatar-url"]; ok { 161 | shared.DiscordWebhook.Avatar_url = v.(string) 162 | } 163 | if v, ok := optionsMap["buffer-webhook-messages"]; ok { 164 | bufferDiscordMessages = v.(bool) 165 | } 166 | if v, ok := optionsMap["retry-webhook-messages"]; ok { 167 | shared.DiscordWebhook.RetrySendingMessages = v.(bool) 168 | } 169 | } 170 | 171 | // EVENTS 172 | 173 | func logEvent(p ActionEventData) { 174 | eventSigHash := p.EventLog.Topics[0] 175 | // event sig hash recognised 176 | if eventAbiInterface, ok := shared.EventSigs[eventSigHash.Hex()]; ok { 177 | eventAbi := eventAbiInterface.(map[string]interface{}) 178 | output, discordOutput := utils.FormatEventInfo(p.EventFields, p.DecodedTopics, p.DecodedData) 179 | 180 | eventInfo := shared.EventInfo{ 181 | Name: eventAbi["name"].(string), 182 | Sig: eventAbi["sig"].(string), 183 | } 184 | 185 | // output to terminal 186 | if logToTerminal { 187 | fmt.Printf( 188 | "Event '%v' %v emitted from %v blocknum %v\n", 189 | eventInfo.Name, 190 | eventInfo.Sig, 191 | p.EventLog.Address, 192 | p.EventLog.BlockNumber, 193 | ) 194 | fmt.Print(output) 195 | } 196 | 197 | // output to discord embed message 198 | if logToDiscord { 199 | logEventToDiscord(discordOutput, eventInfo, p.EventLog) 200 | } 201 | } else { 202 | // event sig hash not recognised 203 | if logToTerminal { 204 | fmt.Printf( 205 | "Unrecognised event emitted from %v, topics %v data %v blocknum %v\n", 206 | p.EventLog.Address, 207 | p.EventLog.Topics, 208 | "0x"+hex.EncodeToString(p.EventLog.Data), 209 | p.EventLog.BlockNumber, 210 | ) 211 | } 212 | } 213 | } 214 | 215 | func logEventToDiscord(discordOutput string, eventInfo shared.EventInfo, vLog types.Log) { 216 | var discordEmbedMsg discordwebhook.Embed 217 | 218 | discordEmbedMsg.Title = eventInfo.Sig 219 | discordEmbedAuthor := discordwebhook.Author{ 220 | Name: vLog.Address.Hex(), 221 | } 222 | fromUrlStr := utils.BlockscanFormat("address", vLog.Address.Hex()) 223 | // if we managed to format the URL (otherwise embed will fail to send) 224 | if strings.HasPrefix(fromUrlStr, "http") { 225 | discordEmbedAuthor.Url = fromUrlStr 226 | } 227 | discordEmbedMsg.Author = discordEmbedAuthor 228 | 229 | TxUrlStr := utils.BlockscanFormat("transaction", vLog.TxHash.Hex()) 230 | if strings.HasPrefix(TxUrlStr, "http") { 231 | discordEmbedMsg.Url = TxUrlStr 232 | } 233 | discordEmbedMsg.Description = discordOutput 234 | discordEmbedMsg.Timestamp = time.Now() 235 | 236 | // unique embed colour based on event signature hash 237 | eventSigHash := vLog.Topics[0] 238 | colourHex := eventSigHash.Hex()[2 : 2+6] 239 | colourNum, _ := strconv.ParseUint(colourHex, 16, 64) 240 | discordEmbedMsg.Color = int(colourNum) 241 | 242 | // if using buffers 243 | if bufferDiscordMessages { 244 | // fmt.Printf("embedBuffer length: %v\n", len(embedBuffer)) 245 | // add embed to embed buffer 246 | embedBuffer = append(embedBuffer, discordEmbedMsg) 247 | if len(embedBuffer) > 10 { 248 | // cant have more than 10 embeds in a single message 249 | // split them up and send them in chunks 250 | for i := 0; i < len(embedBuffer); i += 10 { 251 | var err error 252 | if i+10 > len(embedBuffer) { 253 | err = shared.DiscordWebhook.SendEmbedMessages(embedBuffer[i:]) 254 | } else { 255 | err = shared.DiscordWebhook.SendEmbedMessages(embedBuffer[i : i+10]) 256 | } 257 | if err != nil { 258 | shared.Warnf(slog.Default(), "Webhook error: %v", err) 259 | } 260 | } 261 | embedBuffer = nil // clear buffer after sending them 262 | } else { 263 | err := shared.DiscordWebhook.SendEmbedMessages(embedBuffer) 264 | embedBuffer = nil 265 | if err != nil { 266 | shared.Warnf(slog.Default(), "Webhook error: %v", err) 267 | } 268 | } 269 | } else { 270 | err := shared.DiscordWebhook.SendEmbedMessages([]discordwebhook.Embed{discordEmbedMsg}) 271 | if err != nil { 272 | shared.Warnf(slog.Default(), "Webhook error: %v", err) 273 | } 274 | } 275 | } 276 | 277 | // TRANSACTIONS 278 | 279 | func logTransaction(p ActionTxData) { 280 | if logToTerminal { 281 | logTxToTerminal(p) 282 | } 283 | if logToDiscord { 284 | logTxToDiscord(p) 285 | } 286 | } 287 | 288 | func logTxToTerminal(p ActionTxData) { 289 | tx := p.Transaction 290 | block := p.Block 291 | to := p.To 292 | from := p.From 293 | if utils.IsDeploymentTx(tx) { 294 | deployedContract, _ := shared.GetDeployedContractAddress(tx) 295 | fmt.Printf( 296 | "Deployment TX by %v\n"+ 297 | "contract: %v\n"+ 298 | "value: %v wei (%v ether)\n"+ 299 | "tx hash: %v\n", 300 | from, 301 | deployedContract, 302 | tx.Value(), utils.FormatDecimals(tx.Value(), 18), 303 | tx.Hash(), 304 | ) 305 | } else { 306 | var txTypeStr string 307 | if determineTxType { 308 | txType := shared.DetermineTxType(tx, block.Number()) 309 | txTypeStr = shared.TxTypes[txType] + " TX" 310 | } else { 311 | txTypeStr = "TX" 312 | } 313 | fmt.Printf( 314 | "%v\n"+ 315 | "tx sender: %v\n"+ 316 | "to: %v\n"+ 317 | "value: %v wei (%v ether)\n"+ 318 | "tx hash: %v\n", 319 | txTypeStr, 320 | from, 321 | to, 322 | tx.Value(), utils.FormatDecimals(tx.Value(), 18), 323 | tx.Hash(), 324 | ) 325 | } 326 | } 327 | 328 | // logs to discord embed 329 | func logTxToDiscord(p ActionTxData) { 330 | var discordEmbedMsg discordwebhook.Embed 331 | 332 | tx := p.Transaction 333 | block := p.Block 334 | to := p.To 335 | from := p.From 336 | 337 | // check if deployment tx 338 | if utils.IsDeploymentTx(tx) { 339 | deployedContract, _ := shared.GetDeployedContractAddress(tx) 340 | // embed title 341 | discordEmbedMsg.Title = "Deployment TX" 342 | // embed title link 343 | TxUrlStr := utils.BlockscanFormat("transaction", tx.Hash().Hex()) 344 | if strings.HasPrefix(TxUrlStr, "http") { 345 | discordEmbedMsg.Url = TxUrlStr 346 | } 347 | // embed body 348 | discordEmbedMsg.Description = fmt.Sprintf( 349 | "tx sender: %v\ndeployed contract: %v\nvalue: %v wei (%v ether)", 350 | utils.FormatBlockscanHyperlink("address", from.Hex(), from.Hex()), 351 | utils.FormatBlockscanHyperlink("address", deployedContract.Hex(), deployedContract.Hex()), 352 | tx.Value(), 353 | utils.FormatDecimals(tx.Value(), 18), 354 | ) 355 | // embed time 356 | discordEmbedMsg.Timestamp = time.Now() 357 | // unique embed colour based on tx sender (`from`) 358 | eventSigHash := from 359 | colourHex := eventSigHash.Hex()[2 : 2+6] 360 | colourNum, _ := strconv.ParseUint(colourHex, 16, 64) 361 | discordEmbedMsg.Color = int(colourNum) 362 | 363 | // send embed 364 | err := shared.DiscordWebhook.SendEmbedMessages([]discordwebhook.Embed{discordEmbedMsg}) 365 | if err != nil { 366 | shared.Warnf(slog.Default(), "Webhook error: %v", err) 367 | } 368 | } else { 369 | // log normal tx 370 | 371 | // embed title 372 | var txTypeStr string 373 | if determineTxType { 374 | txType := shared.DetermineTxType(tx, block.Number()) 375 | txTypeStr = shared.TxTypes[txType] + " TX" 376 | } else { 377 | txTypeStr = "TX" 378 | } 379 | discordEmbedMsg.Title = txTypeStr 380 | // embed title link 381 | TxUrlStr := utils.BlockscanFormat("transaction", tx.Hash().Hex()) 382 | if strings.HasPrefix(TxUrlStr, "http") { 383 | discordEmbedMsg.Url = TxUrlStr 384 | } 385 | // embed body 386 | discordEmbedMsg.Description = fmt.Sprintf( 387 | "tx sender: %v\nto: %v\nvalue: %v wei (%v ether)", 388 | utils.FormatBlockscanHyperlink("address", from.Hex(), from.Hex()), 389 | utils.FormatBlockscanHyperlink("address", to.Hex(), to.Hex()), 390 | tx.Value(), 391 | utils.FormatDecimals(tx.Value(), 18), 392 | ) 393 | // embed time 394 | discordEmbedMsg.Timestamp = time.Now() 395 | // unique embed colour based on tx sender (`from`) 396 | eventSigHash := from 397 | colourHex := eventSigHash.Hex()[2 : 2+6] 398 | colourNum, _ := strconv.ParseUint(colourHex, 16, 64) 399 | discordEmbedMsg.Color = int(colourNum) 400 | 401 | // send embed 402 | err := shared.DiscordWebhook.SendEmbedMessages([]discordwebhook.Embed{discordEmbedMsg}) 403 | if err != nil { 404 | shared.Warnf(slog.Default(), "Webhook error: %v", err) 405 | } 406 | } 407 | } 408 | 409 | // BLOCKS 410 | 411 | func logBlockMined(p ActionBlockData) { 412 | block := p.Block 413 | fmt.Printf("Block %v mined\n", block.Number()) 414 | fmt.Printf("Transactions: %v\n", len(block.Transactions())) 415 | 416 | if logToDiscord { 417 | logBlockToDiscord(p) 418 | } 419 | } 420 | 421 | func logBlockToDiscord(p ActionBlockData) { 422 | var discordEmbedMsg discordwebhook.Embed 423 | 424 | discordEmbedMsg.Title = fmt.Sprintf("Block %v mined", p.Block.Number()) 425 | // discordEmbedAuthor := discordwebhook.Author{} 426 | // fromUrlStr := utils.BlockscanFormat("address", vLog.Address.Hex()) 427 | // // if we managed to format the URL (otherwise embed will fail to send) 428 | // if strings.HasPrefix(fromUrlStr, "http") { 429 | // discordEmbedAuthor.Url = fromUrlStr 430 | // } 431 | // discordEmbedMsg.Author = discordEmbedAuthor 432 | 433 | TxUrlStr := utils.BlockscanFormat("block", p.Block.Number().String()) 434 | if strings.HasPrefix(TxUrlStr, "http") { 435 | discordEmbedMsg.Url = TxUrlStr 436 | } 437 | 438 | discordEmbedMsg.Description = fmt.Sprintf( 439 | "Transactions: %v\n"+ 440 | "Time: %v\n", 441 | len(p.Block.Transactions()), 442 | p.Block.Time(), 443 | ) 444 | discordEmbedMsg.Timestamp = time.Now() 445 | 446 | // unique embed colour based on block number 447 | colourHex := fmt.Sprintf("%06v", p.Block.Number().Text(16)[2:])[:6] 448 | colourNum, _ := strconv.ParseUint(colourHex, 16, 64) 449 | discordEmbedMsg.Color = int(colourNum) 450 | 451 | // send embed 452 | err := shared.DiscordWebhook.SendEmbedMessages([]discordwebhook.Embed{discordEmbedMsg}) 453 | if err != nil { 454 | shared.Warnf(slog.Default(), "Webhook error: %v", err) 455 | } 456 | } 457 | 458 | func logDeployments(p ActionBlockData) { 459 | block := p.Block 460 | for _, tx := range block.Transactions() { 461 | // To() returns nil if tx is deployment tx 462 | if tx.To() == nil { 463 | to := tx.To() 464 | from := utils.GetTxSender(tx) 465 | txData := ActionTxData{ 466 | Transaction: tx, 467 | Block: block, 468 | To: to, 469 | From: from, 470 | } 471 | if logToTerminal { 472 | logTxToTerminal(txData) 473 | } 474 | if logToDiscord { 475 | logTxToDiscord(txData) 476 | } 477 | } 478 | } 479 | } 480 | -------------------------------------------------------------------------------- /actions/action_monitor_transfers.go: -------------------------------------------------------------------------------- 1 | package actions 2 | 3 | import ( 4 | "fmt" 5 | "math/big" 6 | "slices" 7 | 8 | "github.com/Zellic/EVM-trackooor/shared" 9 | "github.com/Zellic/EVM-trackooor/utils" 10 | 11 | "github.com/ethereum/go-ethereum/common" 12 | ) 13 | 14 | var monitoredAddresses []common.Address 15 | 16 | func (p action) InitMonitorTransfers() { 17 | // monitor addresses for transactions 18 | monitoredAddresses = p.o.Addresses 19 | for _, addr := range monitoredAddresses { 20 | addTxAddressAction(addr, handleAddressTx) 21 | } 22 | // monitor erc20 token for transfer events 23 | for _, addrInterface := range p.o.CustomOptions["erc20-tokens"].([]interface{}) { 24 | erc20TokenAddress := common.HexToAddress(addrInterface.(string)) 25 | addAddressEventSigAction(erc20TokenAddress, "Transfer(address,address,uint256)", handleTokenTransfer) 26 | } 27 | } 28 | 29 | // called when a tx is from/to monitored address 30 | func handleAddressTx(p ActionTxData) { 31 | from := *p.From 32 | value := p.Transaction.Value() 33 | // tx is from monitored address (since it can be either from/to) 34 | // and value of tx > 0 35 | if slices.Contains(monitoredAddresses, from) && 36 | value.Cmp(big.NewInt(0)) > 0 { 37 | // alert 38 | fmt.Printf("Native ETH Transfer by %v with value %v ETH\n", from, utils.FormatDecimals(value, 18)) 39 | } 40 | } 41 | 42 | // called when erc20 token we're tracking emits Transfer event 43 | func handleTokenTransfer(p ActionEventData) { 44 | from := p.DecodedTopics["from"].(common.Address) 45 | value := p.DecodedData["value"].(*big.Int) 46 | 47 | // erc20 transfer is from an address we're monitoring 48 | if slices.Contains(monitoredAddresses, from) { 49 | // get erc20 token info (to format value decimals + symbol) 50 | token := p.EventLog.Address 51 | tokenInfo := shared.RetrieveERC20Info(token) 52 | decimals := tokenInfo.Decimals 53 | symbol := tokenInfo.Symbol 54 | 55 | // alert 56 | fmt.Printf("ERC20 Transfer by %v with value %v %v\n", from, utils.FormatDecimals(value, decimals), symbol) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /actions/action_ownershiptransfer.go: -------------------------------------------------------------------------------- 1 | package actions 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "math/big" 7 | "strconv" 8 | 9 | "github.com/Zellic/EVM-trackooor/shared" 10 | "github.com/Zellic/EVM-trackooor/utils" 11 | 12 | "github.com/ethereum/go-ethereum/common" 13 | ) 14 | 15 | // logging 16 | var ownershipTransferLog *log.Logger 17 | 18 | var thresholdUSD float64 19 | 20 | // discord webhook 21 | var ownershipTransferWebhook shared.WebhookInstance 22 | 23 | var abr *utils.AddressBalanceRetriever 24 | 25 | func (actionInfo) InfoOwnershipTransferTrack() actionInfo { 26 | name := "OwnershipTransferTrack" 27 | overview := `Listens to OwnershipTransferred events, ` + 28 | `checks contract balance (ETH + specified ERC20s) of contract that emitted the event, ` + 29 | `and logs via discord webhook if the contract balance surpasses a certain threshold` 30 | 31 | description := `Four variations of similar func sigs are tracked: 32 | OwnershipTransferred(address,address) 33 | AdminChanged(address,address) 34 | GovernorTransferred(address,address) 35 | RoleGranted(bytes32,address,address) 36 | 37 | Contract's USD balance is determined by aggregating USD balance of ether and specified ERC20 tokens. 38 | Price of ether and tokens are retrieved using Coin API, which may not support prices for all ERC20 tokens.` 39 | 40 | options := `"threshold" - USD threshold, contract balance above this will cause a discord alert 41 | "coin-api-key" - Coin API key from https://www.coinapi.io/ 42 | "webhook-url" - URL of discord webhook to send discord alerts 43 | "tokens" - array of ERC20 token addresses, to take into account when determining contract USD balance` 44 | 45 | example := `"OwnershipTransferTrack": { 46 | "addresses":{}, 47 | "options":{ 48 | "threshold":10000, 49 | "webhook-url":"https://discord.com/api/webhooks/...", 50 | "coin-api-key":"XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX", 51 | "tokens":["0xdac17f958d2ee523a2206206994597c13d831ec7", 52 | "0xB8c77482e45F1F44dE1745F52C74426C631bDD52", 53 | ...] 54 | } 55 | }` 56 | 57 | return actionInfo{ 58 | ActionName: name, 59 | ActionOverview: overview, 60 | ActionDescription: description, 61 | ActionOptionDetails: options, 62 | ActionConfigExample: example, 63 | } 64 | } 65 | 66 | func (p action) InitOwnershipTransferTrack() { 67 | // logging 68 | ownershipTransferLog = shared.TimeLogger("[OwnershipTransferTrack] ") 69 | 70 | // set threshold 71 | var ok bool 72 | thresholdUSD, ok = p.o.CustomOptions["threshold"].(float64) 73 | if !ok { 74 | ownershipTransferLog.Fatalf("Please specify USD threshold \"threshold\"!") 75 | } 76 | ownershipTransferLog.Printf("Set threshold to $%v USD\n", thresholdUSD) 77 | 78 | // discord webhook 79 | webhookURL, ok := p.o.CustomOptions["webhook-url"].(string) 80 | if !ok { 81 | ownershipTransferLog.Fatalf("Please specify \"webhook-url\"!") 82 | } 83 | ownershipTransferWebhook = shared.WebhookInstance{ 84 | WebhookURL: webhookURL, 85 | Username: "ownership transfer track", 86 | Avatar_url: "", 87 | RetrySendingMessages: true, 88 | } 89 | 90 | // coin api key 91 | coinApiKey, ok := p.o.CustomOptions["coin-api-key"].(string) 92 | if !ok { 93 | ownershipTransferLog.Fatalf("Please specify \"coin-api-key\"!") 94 | } 95 | 96 | // address balance retriever to get USD value of address 97 | var tokens []common.Address 98 | for _, tokenAddrHex := range p.o.CustomOptions["tokens"].([]interface{}) { 99 | tokenAddress := common.HexToAddress(tokenAddrHex.(string)) 100 | tokens = append(tokens, tokenAddress) 101 | } 102 | 103 | abr = utils.NewAddressBalanceRetriever(coinApiKey, tokens) 104 | 105 | /* 106 | 107 | OwnershipTransferred(address indexed previousOwner, address indexed newOwner) 108 | AdminChanged(address indexed previousAdmin, address indexed newAdmin) 109 | GovernorTransferred(address indexed previousGovernor, address indexed newGovernor) 110 | RoleGranted(bytes32 indexed role, address indexed account, address indexed sender) 111 | 112 | */ 113 | addEventSigAction("OwnershipTransferred(address,address)", handleOwnershipTransferred) 114 | addEventSigAction("AdminChanged(address,address)", handleAdminChanged) 115 | addEventSigAction("GovernorTransferred(address,address)", handleGovernorTransferred) 116 | addEventSigAction("RoleGranted(bytes32,address,address)", handleRoleGranted) 117 | 118 | } 119 | 120 | func handleOwnershipTransferred(p ActionEventData) { 121 | // validate decoded topics contains what we're expecting 122 | _, ok := p.DecodedTopics["previousOwner"] 123 | if !ok { 124 | return 125 | } 126 | _, ok = p.DecodedTopics["newOwner"] 127 | if !ok { 128 | return 129 | } 130 | 131 | previousOwner := p.DecodedTopics["previousOwner"].(common.Address) 132 | newOwner := p.DecodedTopics["newOwner"].(common.Address) 133 | 134 | // ignore if previously no owner (e.g contract init) 135 | if previousOwner.Cmp(shared.ZeroAddress) == 0 { 136 | fmt.Printf("zero addr, ignoring - previousOwner: %v\n", previousOwner) 137 | return 138 | } 139 | 140 | contract := p.EventLog.Address 141 | txHash := p.EventLog.TxHash 142 | blockNum := big.NewInt(int64(p.EventLog.BlockNumber)) 143 | 144 | contractBalanceUSD := abr.GetAddressUSDbalance(contract, blockNum) 145 | 146 | // ownershipTransferLog.Printf("contract: %v txhash: %v previousOwner: %v newOwner: %v\n", contract, txHash, previousOwner, newOwner) 147 | // ownershipTransferLog.Printf("contractBalanceUSD: %v\n", contractBalanceUSD) 148 | 149 | if contractBalanceUSD > thresholdUSD { 150 | ownershipTransferLog.Printf("sending discord msg OwnershipTransferred\n") 151 | ownershipTransferLog.Printf("contractBalanceUSD: %v\n", contractBalanceUSD) 152 | 153 | ownershipTransferWebhook.SendMessage(fmt.Sprintf(`## OwnershipTransferred event 154 | %v %v 155 | previousOwner: %v 156 | newOwner: %v 157 | balance (USD): %v`, 158 | utils.FormatBlockscanHyperlink("address", "`contract`", contract.Hex()), 159 | utils.FormatBlockscanHyperlink("transaction", "`tx hash`", txHash.Hex()), 160 | utils.FormatBlockscanHyperlink("address", utils.ShortenAddress(previousOwner), previousOwner.Hex()), 161 | utils.FormatBlockscanHyperlink("address", utils.ShortenAddress(newOwner), newOwner.Hex()), 162 | utils.CodeQuote(strconv.FormatFloat(contractBalanceUSD, 'f', -1, 64))), 163 | ) 164 | ownershipTransferLog.Printf("discord msg sent\n") 165 | } 166 | } 167 | 168 | func handleAdminChanged(p ActionEventData) { 169 | // validate decoded topics contains what we're expecting 170 | _, ok := p.DecodedTopics["previousAdmin"] 171 | if !ok { 172 | return 173 | } 174 | _, ok = p.DecodedTopics["newAdmin"] 175 | if !ok { 176 | return 177 | } 178 | 179 | previousAdmin := p.DecodedTopics["previousAdmin"].(common.Address) 180 | newAdmin := p.DecodedTopics["newAdmin"].(common.Address) 181 | 182 | // ignore if previously no owner (e.g contract init) 183 | if previousAdmin.Cmp(shared.ZeroAddress) == 0 { 184 | fmt.Printf("zero addr, ignoring - previousAdmin: %v\n", previousAdmin) 185 | return 186 | } 187 | 188 | contract := p.EventLog.Address 189 | txHash := p.EventLog.TxHash 190 | blockNum := big.NewInt(int64(p.EventLog.BlockNumber)) 191 | 192 | contractBalanceUSD := abr.GetAddressUSDbalance(contract, blockNum) 193 | 194 | // ownershipTransferLog.Printf("contract: %v txhash: %v previousOwner: %v newOwner: %v\n", contract, txHash, previousOwner, newOwner) 195 | // ownershipTransferLog.Printf("contractBalanceUSD: %v\n", contractBalanceUSD) 196 | 197 | if contractBalanceUSD > thresholdUSD { 198 | ownershipTransferLog.Printf("sending discord msg AdminChanged\n") 199 | ownershipTransferLog.Printf("contractBalanceUSD: %v\n", contractBalanceUSD) 200 | 201 | ownershipTransferWebhook.SendMessage(fmt.Sprintf(`## AdminChanged event 202 | %v %v 203 | previousAdmin: %v 204 | newAdmin: %v 205 | balance (USD): %v`, 206 | utils.FormatBlockscanHyperlink("address", "`contract`", contract.Hex()), 207 | utils.FormatBlockscanHyperlink("transaction", "`tx hash`", txHash.Hex()), 208 | utils.FormatBlockscanHyperlink("address", utils.ShortenAddress(previousAdmin), previousAdmin.Hex()), 209 | utils.FormatBlockscanHyperlink("address", utils.ShortenAddress(newAdmin), newAdmin.Hex()), 210 | utils.CodeQuote(strconv.FormatFloat(contractBalanceUSD, 'f', -1, 64))), 211 | ) 212 | ownershipTransferLog.Printf("discord msg sent\n") 213 | } 214 | } 215 | 216 | func handleGovernorTransferred(p ActionEventData) { 217 | // validate decoded topics contains what we're expecting 218 | _, ok := p.DecodedTopics["previousGovernor"] 219 | if !ok { 220 | return 221 | } 222 | _, ok = p.DecodedTopics["newGovernor"] 223 | if !ok { 224 | return 225 | } 226 | 227 | previousGovernor := p.DecodedTopics["previousGovernor"].(common.Address) 228 | newGovernor := p.DecodedTopics["newGovernor"].(common.Address) 229 | 230 | // ignore if previously no owner (e.g contract init) 231 | if previousGovernor.Cmp(shared.ZeroAddress) == 0 { 232 | fmt.Printf("zero addr, ignoring - previousGovernor: %v\n", previousGovernor) 233 | return 234 | } 235 | 236 | contract := p.EventLog.Address 237 | txHash := p.EventLog.TxHash 238 | blockNum := big.NewInt(int64(p.EventLog.BlockNumber)) 239 | 240 | contractBalanceUSD := abr.GetAddressUSDbalance(contract, blockNum) 241 | 242 | // ownershipTransferLog.Printf("contract: %v txhash: %v previousOwner: %v newOwner: %v\n", contract, txHash, previousOwner, newOwner) 243 | // ownershipTransferLog.Printf("contractBalanceUSD: %v\n", contractBalanceUSD) 244 | 245 | if contractBalanceUSD > thresholdUSD { 246 | ownershipTransferLog.Printf("sending discord msg GovernorTransferred\n") 247 | ownershipTransferLog.Printf("contractBalanceUSD: %v\n", contractBalanceUSD) 248 | 249 | ownershipTransferWebhook.SendMessage(fmt.Sprintf(`## GovernorTransferred event 250 | %v %v 251 | previousGovernor: %v 252 | newGovernor: %v 253 | balance (USD): %v`, 254 | utils.FormatBlockscanHyperlink("address", "`contract`", contract.Hex()), 255 | utils.FormatBlockscanHyperlink("transaction", "`tx hash`", txHash.Hex()), 256 | utils.FormatBlockscanHyperlink("address", utils.ShortenAddress(previousGovernor), previousGovernor.Hex()), 257 | utils.FormatBlockscanHyperlink("address", utils.ShortenAddress(newGovernor), newGovernor.Hex()), 258 | utils.CodeQuote(strconv.FormatFloat(contractBalanceUSD, 'f', -1, 64))), 259 | ) 260 | ownershipTransferLog.Printf("discord msg sent\n") 261 | } 262 | } 263 | 264 | func handleRoleGranted(p ActionEventData) { 265 | // validate decoded topics contains what we're expecting 266 | _, ok := p.DecodedTopics["previousGovernor"] 267 | if !ok { 268 | return 269 | } 270 | _, ok = p.DecodedTopics["newGovernor"] 271 | if !ok { 272 | return 273 | } 274 | _, ok = p.DecodedTopics["role"] 275 | if !ok { 276 | return 277 | } 278 | 279 | role := p.DecodedTopics["role"].([32]byte) 280 | account := p.DecodedTopics["account"].(common.Address) 281 | sender := p.DecodedTopics["sender"].(common.Address) 282 | 283 | contract := p.EventLog.Address 284 | txHash := p.EventLog.TxHash 285 | blockNum := big.NewInt(int64(p.EventLog.BlockNumber)) 286 | 287 | contractBalanceUSD := abr.GetAddressUSDbalance(contract, blockNum) 288 | 289 | // ownershipTransferLog.Printf("contract: %v txhash: %v previousOwner: %v newOwner: %v\n", contract, txHash, previousOwner, newOwner) 290 | // ownershipTransferLog.Printf("contractBalanceUSD: %v\n", contractBalanceUSD) 291 | 292 | if contractBalanceUSD > thresholdUSD { 293 | ownershipTransferLog.Printf("sending discord msg RoleGranted\n") 294 | ownershipTransferLog.Printf("contractBalanceUSD: %v\n", contractBalanceUSD) 295 | 296 | ownershipTransferWebhook.SendMessage(fmt.Sprintf(`## RoleGranted event 297 | %v %v 298 | account: %v 299 | sender: %v 300 | role: %v 301 | balance (USD): %v`, 302 | utils.FormatBlockscanHyperlink("address", "`contract`", contract.Hex()), 303 | utils.FormatBlockscanHyperlink("transaction", "`tx hash`", txHash.Hex()), 304 | utils.FormatBlockscanHyperlink("address", utils.ShortenAddress(account), account.Hex()), 305 | utils.FormatBlockscanHyperlink("address", utils.ShortenAddress(sender), sender.Hex()), 306 | utils.CodeQuote(common.Bytes2Hex(role[:])), 307 | utils.CodeQuote(strconv.FormatFloat(contractBalanceUSD, 'f', -1, 64))), 308 | ) 309 | ownershipTransferLog.Printf("discord msg sent\n") 310 | } 311 | } 312 | -------------------------------------------------------------------------------- /actions/action_proxy_upgrades.go: -------------------------------------------------------------------------------- 1 | package actions 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "strconv" 8 | 9 | "github.com/Zellic/EVM-trackooor/shared" 10 | "github.com/Zellic/EVM-trackooor/utils" 11 | 12 | "github.com/ethereum/go-ethereum/common" 13 | ) 14 | 15 | var proxyUpgradesConfig struct { 16 | thresholdUSD float64 17 | webhook shared.WebhookInstance 18 | coinApiKey string 19 | } 20 | 21 | var proxyUpgradesLog *log.Logger 22 | 23 | func (actionInfo) InfoProxyUpgrades() actionInfo { 24 | name := "ProxyUpgrades" 25 | overview := `Listens to proxy upgrade events, ` + 26 | `checks contract balance (ETH + specified ERC20s) of contract that emitted the event, ` + 27 | `and logs via discord webhook if the contract balance surpasses a certain threshold` 28 | 29 | description := `The event signature Upgraded(address) is monitored. 30 | 31 | Contract's USD balance is determined by aggregating USD balance of ether and specified ERC20 tokens. 32 | Price of ether and tokens are retrieved using Coin API, which may not support prices for all ERC20 tokens.` 33 | 34 | options := `"threshold" - USD threshold, contract balance above this will cause a discord alert 35 | "coin-api-key" - Coin API key from https://www.coinapi.io/ 36 | "webhook-url" - URL of discord webhook to send discord alerts 37 | "tokens" - array of ERC20 token addresses, to take into account when determining contract USD balance` 38 | 39 | example := `"ProxyUpgrades": { 40 | "addresses":{}, 41 | "options":{ 42 | "threshold":10000, 43 | "webhook-url":"https://discord.com/api/webhooks/...", 44 | "coin-api-key":"XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX", 45 | "tokens":["0xdac17f958d2ee523a2206206994597c13d831ec7", 46 | "0xB8c77482e45F1F44dE1745F52C74426C631bDD52", 47 | ...] 48 | } 49 | }` 50 | 51 | return actionInfo{ 52 | ActionName: name, 53 | ActionOverview: overview, 54 | ActionDescription: description, 55 | ActionOptionDetails: options, 56 | ActionConfigExample: example, 57 | } 58 | } 59 | 60 | func (p action) InitProxyUpgrades() { 61 | // logging 62 | proxyUpgradesLog = shared.TimeLogger("[Proxy Upgrades] ") 63 | 64 | // load config 65 | loadProxyUpgradesConfig(p.o.CustomOptions) 66 | 67 | // address balance retriever to get USD value of address 68 | var tokens []common.Address 69 | for _, tokenAddrHex := range p.o.CustomOptions["tokens"].([]interface{}) { 70 | tokenAddress := common.HexToAddress(tokenAddrHex.(string)) 71 | tokens = append(tokens, tokenAddress) 72 | } 73 | 74 | abr = utils.NewAddressBalanceRetriever(proxyUpgradesConfig.coinApiKey, tokens) 75 | 76 | // track Upgraded(address indexed implementation) 77 | // this is emitted for both Transparent and UPPS proxies 78 | // (at least when using OpenZepplin's code) 79 | addEventSigAction("Upgraded(address)", handleProxyUpgrade) 80 | } 81 | 82 | func loadProxyUpgradesConfig(customOptions map[string]interface{}) { 83 | if v, ok := customOptions["coin-api-key"]; ok { 84 | proxyUpgradesConfig.coinApiKey = v.(string) 85 | } else { 86 | proxyUpgradesLog.Fatalf("Please specify \"coin-api-key\"!") 87 | } 88 | 89 | if v, ok := customOptions["threshold"]; ok { 90 | proxyUpgradesConfig.thresholdUSD = v.(float64) 91 | } else { 92 | proxyUpgradesLog.Fatalf("Please specify USD threshold \"threshold\"!") 93 | } 94 | 95 | if v, ok := customOptions["webhook-url"]; ok { 96 | proxyUpgradesConfig.webhook = shared.WebhookInstance{ 97 | WebhookURL: v.(string), 98 | Username: "proxy upgrade track", 99 | Avatar_url: "", 100 | RetrySendingMessages: true, 101 | } 102 | } else { 103 | proxyUpgradesLog.Fatalf("Please specify discord webhook URL \"webhook-url\"!") 104 | } 105 | 106 | proxyUpgradesLog.Printf("Set threshold to $%v USD\n", proxyUpgradesConfig.thresholdUSD) 107 | } 108 | 109 | func handleProxyUpgrade(p ActionEventData) { 110 | // validate decoded event have fields we're expecting 111 | if _, ok := p.DecodedTopics["implementation"]; !ok { 112 | return 113 | } 114 | 115 | proxy := p.EventLog.Address 116 | upgradedTo := p.DecodedTopics["implementation"].(common.Address) 117 | fmt.Printf("proxy %v upgraded to %v\n", proxy, upgradedTo) 118 | 119 | header, err := shared.Client.HeaderByHash(context.Background(), p.EventLog.BlockHash) 120 | if err != nil { 121 | proxyUpgradesLog.Printf("err when getting header: %v\n", err) 122 | } 123 | blockNum := header.Number 124 | 125 | contractUSDbalance := abr.GetAddressUSDbalance(proxy, blockNum) 126 | fmt.Printf("proxy usd bal: %v\n", contractUSDbalance) 127 | if contractUSDbalance >= proxyUpgradesConfig.thresholdUSD { 128 | fmt.Printf("proxy %v exceeds threshold usd, usd: %v upgraded to %v\n", proxy, contractUSDbalance, upgradedTo) 129 | // send discord webhook msg 130 | proxyUpgradesConfig.webhook.SendMessage(fmt.Sprintf(`## Proxy Upgraded 131 | proxy: %v 132 | upgradedTo: %v 133 | proxy USD balance: %v`, 134 | utils.FormatBlockscanHyperlink("address", utils.ShortenAddress(proxy), proxy.Hex()), 135 | utils.FormatBlockscanHyperlink("address", utils.ShortenAddress(upgradedTo), upgradedTo.Hex()), 136 | utils.CodeQuote(strconv.FormatFloat(contractUSDbalance, 'f', -1, 64)), 137 | )) 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /actions/action_send_transaction.go: -------------------------------------------------------------------------------- 1 | package actions 2 | 3 | import ( 4 | "context" 5 | "crypto/ecdsa" 6 | "fmt" 7 | "log" 8 | "math/big" 9 | 10 | "github.com/Zellic/EVM-trackooor/shared" 11 | 12 | "github.com/ethereum/go-ethereum/common" 13 | "github.com/ethereum/go-ethereum/core/types" 14 | "github.com/ethereum/go-ethereum/crypto" 15 | ) 16 | 17 | var privateKeyHex string 18 | var myNonce uint64 19 | var gasPriceSuggested *big.Int 20 | 21 | func (p action) InitSendTransactionTest() { 22 | 23 | privateKeyHex = p.o.CustomOptions["private-key"].(string) 24 | 25 | // get private key 26 | privateKey, err := crypto.HexToECDSA(privateKeyHex) 27 | if err != nil { 28 | panic(err) 29 | } 30 | 31 | // get public key 32 | publicKey := privateKey.Public() 33 | publicKeyECDSA, ok := publicKey.(*ecdsa.PublicKey) 34 | if !ok { 35 | log.Fatal("error casting public key to ECDSA") 36 | } 37 | 38 | fromAddress := crypto.PubkeyToAddress(*publicKeyECDSA) 39 | 40 | updateNonce(fromAddress) 41 | 42 | // get suggested gas price 43 | gasPriceSuggested, err = shared.Client.SuggestGasPrice(context.Background()) 44 | if err != nil { 45 | panic(err) 46 | } 47 | 48 | for _, address := range p.o.Addresses { 49 | addTxAddressAction(address, TryBackrunTx) 50 | } 51 | 52 | // fix stuck txs by sending 0 eth to self 53 | fmt.Printf("fix stuck txs - sending 0 eth to self\n") 54 | sendEtherToSelf(privateKeyHex, big.NewInt(0), 2_000_000, gasPriceSuggested) 55 | 56 | myNonce += 1 57 | 58 | // TEMPORARY TESTING 59 | 60 | // (uncomment below to test just sending a tx immediantly after querying block) 61 | // gasPrice, err := shared.Client.SuggestGasPrice(context.Background()) 62 | // if err != nil { 63 | // panic(err) 64 | // } 65 | 66 | // pendingBlock, err := shared.GetL2BlockByString("pending") 67 | // if err != nil { 68 | // panic(err) 69 | // } 70 | // fmt.Printf("pending block num: %v\n", pendingBlock.Number()) 71 | // sendEtherToSelf(privateKeyHex, big.NewInt(1337), 2_000_000, gasPrice) 72 | } 73 | 74 | func updateNonce(addr common.Address) { 75 | nonce, err := shared.Client.PendingNonceAt(context.Background(), addr) 76 | if err != nil { 77 | log.Fatal(err) 78 | } 79 | myNonce = nonce 80 | } 81 | 82 | func sendEtherToSelf(privateKeyHex string, weiAmount *big.Int, gasLimit uint64, gasPrice *big.Int) { 83 | // get private key 84 | privateKey, err := crypto.HexToECDSA(privateKeyHex) 85 | if err != nil { 86 | panic(err) 87 | } 88 | 89 | // get public key 90 | publicKey := privateKey.Public() 91 | publicKeyECDSA, ok := publicKey.(*ecdsa.PublicKey) 92 | if !ok { 93 | log.Fatal("error casting public key to ECDSA") 94 | } 95 | 96 | fromAddress := crypto.PubkeyToAddress(*publicKeyECDSA) 97 | 98 | value := weiAmount // in wei (1 eth) 99 | 100 | nonce := myNonce 101 | 102 | // send eth to self 103 | toAddress := fromAddress 104 | var data []byte 105 | tx := types.NewTransaction(nonce, toAddress, value, gasLimit, gasPrice, data) 106 | 107 | chainID := shared.ChainID 108 | 109 | signedTx, err := types.SignTx(tx, types.NewEIP155Signer(chainID), privateKey) 110 | if err != nil { 111 | log.Fatal(err) 112 | } 113 | 114 | // fmt.Printf("sending ether to self\n") 115 | // fmt.Printf("value: %v\n", value) 116 | // fmt.Printf("gasLimit: %v\n", gasLimit) 117 | // fmt.Printf("gasPrice: %v\n", gasPrice) 118 | err = shared.Client.SendTransaction(context.Background(), signedTx) 119 | if err != nil { 120 | log.Fatal(err) 121 | } 122 | 123 | fmt.Printf("tx sent: %s\n", signedTx.Hash().Hex()) 124 | } 125 | 126 | func TryBackrunTx(p ActionTxData) { 127 | fmt.Printf("trying to backrun Tx: %v\n", p.Transaction.Hash()) 128 | fmt.Printf("Block: %v\n", p.Block.Number()) 129 | fmt.Printf("from: %v\n", p.From) 130 | fmt.Printf("to: %v\n", p.To) 131 | fmt.Printf("Sending ether to self...\n") 132 | 133 | // sendEtherToSelf( 134 | // privateKeyHex, 135 | // big.NewInt(0), 136 | // 20052347, 137 | // gasPrice, 138 | // ) 139 | 140 | maxPriorityFee := big.NewInt(1000000) 141 | 142 | newSendEthToSelf( 143 | privateKeyHex, 144 | big.NewInt(31), 145 | big.NewInt(0).Add(gasPriceSuggested, maxPriorityFee), 146 | gasPriceSuggested, 147 | maxPriorityFee, 148 | ) 149 | 150 | } 151 | 152 | func newSendEthToSelf(privateKeyHex string, weiAmount *big.Int, gasLimit *big.Int, gasPrice *big.Int, maxPriorityFee *big.Int) { 153 | // get private key 154 | privateKey, err := crypto.HexToECDSA(privateKeyHex) 155 | if err != nil { 156 | panic(err) 157 | } 158 | 159 | // get public key 160 | publicKey := privateKey.Public() 161 | publicKeyECDSA, ok := publicKey.(*ecdsa.PublicKey) 162 | if !ok { 163 | log.Fatal("error casting public key to ECDSA") 164 | } 165 | 166 | fromAddress := crypto.PubkeyToAddress(*publicKeyECDSA) 167 | 168 | value := weiAmount // in wei 169 | 170 | // send eth to self 171 | toAddress := fromAddress 172 | txData := &types.DynamicFeeTx{ 173 | ChainID: shared.ChainID, 174 | Nonce: myNonce, 175 | GasTipCap: maxPriorityFee, 176 | GasFeeCap: gasLimit, 177 | Gas: 21000, 178 | To: &toAddress, 179 | Value: value, 180 | Data: nil, 181 | AccessList: nil, 182 | } 183 | signedTx, err := types.SignNewTx( 184 | privateKey, 185 | types.LatestSignerForChainID(shared.ChainID), 186 | txData, 187 | ) 188 | if err != nil { 189 | panic(err) 190 | } 191 | 192 | // fmt.Printf("sending ether to self\n") 193 | // fmt.Printf("value: %v\n", value) 194 | // fmt.Printf("gasLimit: %v\n", gasLimit) 195 | // fmt.Printf("gasPrice: %v\n", gasPrice) 196 | // fmt.Printf("maxPriorityFee: %v\n", maxPriorityFee) 197 | err = shared.Client.SendTransaction(context.Background(), signedTx) 198 | if err != nil { 199 | log.Fatal(err) 200 | } 201 | 202 | fmt.Printf("tx sent: %s\n", signedTx.Hash().Hex()) 203 | // updateNonce(fromAddress) 204 | myNonce += 1 205 | } 206 | -------------------------------------------------------------------------------- /actions/action_tethertrack.go: -------------------------------------------------------------------------------- 1 | package actions 2 | 3 | import ( 4 | "fmt" 5 | "math/big" 6 | 7 | "github.com/Zellic/EVM-trackooor/utils" 8 | 9 | "github.com/ethereum/go-ethereum/common" 10 | ) 11 | 12 | func (actionInfo) InfoTetherTrack() actionInfo { 13 | name := "TetherTrack" 14 | overview := "Simple, example action that logs tether transfers of over 100,000 USDT" 15 | 16 | description := "This is done by listening to Transfer events and checking the value field." 17 | 18 | options := `No options` 19 | 20 | example := `"TetherTrack": { 21 | "addresses": {}, 22 | "options":{} 23 | }` 24 | 25 | return actionInfo{ 26 | ActionName: name, 27 | ActionOverview: overview, 28 | ActionDescription: description, 29 | ActionOptionDetails: options, 30 | ActionConfigExample: example, 31 | } 32 | } 33 | 34 | func (p action) InitTetherTrack() { 35 | tether := common.HexToAddress("0xdAC17F958D2ee523a2206206994597C13D831ec7") 36 | addAddressEventSigAction(tether, "Transfer(address,address,uint256)", handleTetherTransfer) 37 | } 38 | 39 | func handleTetherTransfer(p ActionEventData) { 40 | to := p.DecodedTopics["to"].(common.Address) 41 | from := p.DecodedTopics["from"].(common.Address) 42 | value := p.DecodedData["value"].(*big.Int) 43 | 44 | threshold, _ := big.NewInt(0).SetString("100000000000", 10) // 100k USDT 45 | 46 | if value.Cmp(threshold) >= 0 { 47 | fmt.Printf( 48 | "Threshold exceeded, from: %v to: %v value: %v txhash: %v\n", 49 | to, from, utils.FormatDecimals(value, 6), p.EventLog.TxHash, 50 | ) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /actions/action_tornadocash.go: -------------------------------------------------------------------------------- 1 | package actions 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "math/big" 8 | "os" 9 | "sync" 10 | 11 | "github.com/Zellic/EVM-trackooor/utils" 12 | 13 | "github.com/Zellic/EVM-trackooor/shared" 14 | 15 | "github.com/ethereum/go-ethereum/common" 16 | ) 17 | 18 | type tornadoInteractions struct { 19 | Name string 20 | DepositCallers []common.Address 21 | WithdrawToAddresses []common.Address 22 | } 23 | 24 | type tornadoContractsMutex struct { 25 | contracts map[common.Address]tornadoInteractions 26 | mutex sync.Mutex 27 | } 28 | 29 | var tornadoContractsHistorical tornadoContractsMutex 30 | 31 | func (actionInfo) InfoTornadoCash() actionInfo { 32 | name := "TornadoCash" 33 | overview := "Records historical Tornado.Cash deposits and withdrawals, using emitted events." 34 | description := "Recorded data include tx sender of Deposit event emitted transactions, " + 35 | "and the withdrawTo field of withdrawal events, which is the address funds were sent to. \n\n" + 36 | "Addresses provided should be Tornado.Cash contract addresses." 37 | 38 | options := `"output-filepath" - where to output the JSON data to, default ./tornadoCashAddresses.json` 39 | 40 | example := `"TornadoCash": { 41 | "addresses": { 42 | "0x12D66f87A04A9E220743712cE6d9bB1B5616B8Fc": {"name": "Tornado.Cash 0.1 ETH"} 43 | }, 44 | "options":{ 45 | "output-filepath":"./tornadoCashAddresses.json" 46 | } 47 | }` 48 | 49 | return actionInfo{ 50 | ActionName: name, 51 | ActionOverview: overview, 52 | ActionDescription: description, 53 | ActionOptionDetails: options, 54 | ActionConfigExample: example, 55 | } 56 | } 57 | 58 | func (t *tornadoContractsMutex) addDepositCaller(contract common.Address, caller common.Address) { 59 | t.mutex.Lock() 60 | defer t.mutex.Unlock() 61 | 62 | tornadoInteraction := t.contracts[contract] 63 | tornadoInteraction.DepositCallers = append(tornadoInteraction.DepositCallers, caller) 64 | t.contracts[contract] = tornadoInteraction 65 | } 66 | 67 | func (t *tornadoContractsMutex) addWithdrawToAddress(contract common.Address, withdrawTo common.Address) { 68 | t.mutex.Lock() 69 | defer t.mutex.Unlock() 70 | 71 | tornadoInteraction := t.contracts[contract] 72 | tornadoInteraction.WithdrawToAddresses = append(tornadoInteraction.WithdrawToAddresses, withdrawTo) 73 | t.contracts[contract] = tornadoInteraction 74 | } 75 | 76 | func InitTornadoContracts() tornadoContractsMutex { 77 | return tornadoContractsMutex{contracts: make(map[common.Address]tornadoInteractions)} 78 | } 79 | 80 | // init function called just before starting trackooor 81 | func (action) InitTornadoCash() { 82 | tornadoContractsHistorical = InitTornadoContracts() 83 | addEventSigAction("Withdrawal(address,bytes32,address,uint256)", ProcessTornadoWithdraw) 84 | addEventSigAction("Deposit(bytes32,uint32,uint256)", ProcessTornadoDeposit) 85 | } 86 | 87 | // function called after historical processor is finished 88 | func (p action) FinishedTornadoCash() { 89 | // wait for async funcs to finish 90 | RpcWaitGroup.Wait() 91 | 92 | var writeData []byte 93 | 94 | // get tornado contract names from config 95 | for tornadoAddress := range tornadoContractsHistorical.contracts { 96 | name := shared.Options.AddressProperties[tornadoAddress]["name"].(string) 97 | 98 | tornadoInteractions := tornadoContractsHistorical.contracts[tornadoAddress] 99 | tornadoInteractions.Name = name 100 | tornadoContractsHistorical.contracts[tornadoAddress] = tornadoInteractions 101 | } 102 | 103 | withdrawToAddressesJSON, _ := json.Marshal( 104 | struct { 105 | FromBlock *big.Int 106 | ToBlock *big.Int 107 | TornadoContracts map[common.Address]tornadoInteractions 108 | }{ 109 | FromBlock: shared.Options.HistoricalOptions.FromBlock, 110 | ToBlock: shared.Options.HistoricalOptions.ToBlock, 111 | TornadoContracts: tornadoContractsHistorical.contracts, 112 | }, 113 | ) 114 | writeData = withdrawToAddressesJSON 115 | 116 | var outputFilepath string 117 | if v, ok := p.o.CustomOptions["output-filepath"]; ok { 118 | outputFilepath = v.(string) 119 | } else { 120 | outputFilepath = "./tornadoCashAddresses.json" 121 | fmt.Printf("TornadoCash: Output filepath not specified, using default of %v\n", outputFilepath) 122 | } 123 | 124 | err := os.WriteFile(outputFilepath, writeData, 0644) 125 | if err != nil { 126 | panic(err) 127 | } 128 | } 129 | 130 | func ProcessTornadoWithdraw(p ActionEventData) { 131 | // validate decoded event have fields we're expecting 132 | if _, ok := p.DecodedData["to"]; !ok { 133 | return 134 | } 135 | 136 | // record address funds were withdrawn to 137 | contractAddress := p.EventLog.Address 138 | to := p.DecodedData["to"].(common.Address) 139 | 140 | tornadoContractsHistorical.addWithdrawToAddress(contractAddress, to) 141 | } 142 | 143 | func ProcessTornadoDeposit(p ActionEventData) { 144 | // record address of depositer 145 | RpcWaitGroup.Add(1) 146 | go func() { 147 | contractAddress := p.EventLog.Address 148 | 149 | // query for tx sender 150 | txHash := p.EventLog.TxHash 151 | tx, _, err := shared.Client.TransactionByHash(context.Background(), txHash) 152 | if err != nil { 153 | panic(err) 154 | } 155 | sender := utils.GetTxSender(tx) 156 | 157 | // record tx sender 158 | tornadoContractsHistorical.addDepositCaller(contractAddress, *sender) 159 | 160 | RpcWaitGroup.Done() 161 | }() 162 | } 163 | -------------------------------------------------------------------------------- /actions/action_track_contract_calls.go: -------------------------------------------------------------------------------- 1 | package actions 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | 7 | "github.com/Zellic/EVM-trackooor/shared" 8 | 9 | "github.com/ethereum/go-ethereum/crypto" 10 | ) 11 | 12 | // map func sigs to their corresponding hashes 13 | var trackedFuncSigs map[string][]byte 14 | 15 | func (p action) InitTrackContractCalls() { 16 | trackedFuncSigs = make(map[string][]byte) 17 | 18 | for _, addr := range shared.Options.FilterAddresses { 19 | addTxAddressAction(addr, checkFunctionCall) 20 | } 21 | 22 | funcSigsInterface := p.o.CustomOptions["function-signatures"].([]interface{}) 23 | for _, funcSigInterface := range funcSigsInterface { 24 | funcSig := funcSigInterface.(string) 25 | funcSigHash := crypto.Keccak256([]byte(funcSig))[:4] 26 | trackedFuncSigs[funcSig] = funcSigHash 27 | } 28 | } 29 | 30 | func checkFunctionCall(p ActionTxData) { 31 | // continue only if tx was contract interaction 32 | txType := shared.DetermineTxType(p.Transaction, p.Block.Number()) 33 | if txType != shared.ContractTx { 34 | return 35 | } 36 | 37 | // get tx To and From 38 | to := p.To 39 | from := p.From 40 | 41 | // get tx calldata 42 | txData := p.Transaction.Data() 43 | // get tx func selector 44 | txFuncSelector := txData[:4] 45 | 46 | // log if matches tracked func sig 47 | for trackedFuncSig, trackedFuncSigHash := range trackedFuncSigs { 48 | if bytes.Equal(trackedFuncSigHash, txFuncSelector) { 49 | fmt.Printf( 50 | "%v called function %v on contract %v\n", 51 | from, 52 | trackedFuncSig, 53 | to, 54 | ) 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /actions/base.go: -------------------------------------------------------------------------------- 1 | package actions 2 | 3 | import ( 4 | "log" 5 | "log/slog" 6 | "reflect" 7 | "slices" 8 | "sync" 9 | 10 | "github.com/Zellic/EVM-trackooor/shared" 11 | 12 | "github.com/ethereum/go-ethereum/common" 13 | "github.com/ethereum/go-ethereum/crypto" 14 | ) 15 | 16 | type action struct { 17 | o shared.ActionOptions 18 | } 19 | 20 | type actionInfo struct { 21 | ActionName string 22 | ActionOverview string 23 | ActionDescription string 24 | ActionOptionDetails string 25 | ActionConfigExample string 26 | } 27 | 28 | // prevent concurrent map writes for maps: 29 | // EventSigToAction 30 | // ContractToEventSigToAction 31 | // TxAddressToAction 32 | var ActionMapMutex sync.RWMutex 33 | 34 | // add an action function to the event sig -> func mapping 35 | func addEventSigAction(sig string, f func(ActionEventData)) { 36 | // ensure ABI for event sig exists 37 | sighash := crypto.Keccak256Hash([]byte(sig)) 38 | _, ok := shared.EventSigs[sighash.Hex()] 39 | if !ok { 40 | log.Fatalf("Event ABI for '%v', does not exist, please add its ABI to the event signatures data file", sig) 41 | } 42 | 43 | ActionMapMutex.Lock() 44 | 45 | // make sure action func is not already in array 46 | if !containsActionEventFunc(EventSigToAction[sighash], f) { 47 | EventSigToAction[sighash] = append(EventSigToAction[sighash], f) 48 | } 49 | 50 | // add event sig to filter topics if not already added 51 | if len(shared.Options.FilterEventTopics) == 0 { 52 | shared.Options.FilterEventTopics = [][]common.Hash{{}} 53 | } 54 | if !slices.Contains(shared.Options.FilterEventTopics[0], sighash) { 55 | shared.Options.FilterEventTopics[0] = append(shared.Options.FilterEventTopics[0], sighash) 56 | } 57 | 58 | ActionMapMutex.Unlock() 59 | } 60 | 61 | // add an action function to the contract address, event sig -> func mapping 62 | func addAddressEventSigAction(contractAddress common.Address, sig string, f func(ActionEventData)) { 63 | // ensure ABI for event sig exists 64 | sighash := crypto.Keccak256Hash([]byte(sig)) 65 | _, ok := shared.EventSigs[sighash.Hex()] 66 | if !ok { 67 | log.Fatalf("Event ABI for '%v', does not exist, please add its ABI to the event signatures data file", sig) 68 | } 69 | 70 | // check if inner map is already initialized. if not, initialise it 71 | ActionMapMutex.Lock() 72 | if ContractToEventSigToAction[contractAddress] == nil { 73 | ContractToEventSigToAction[contractAddress] = make(map[common.Hash][]func(ActionEventData)) 74 | } 75 | 76 | // make sure action func is not already in array 77 | if !containsActionEventFunc(ContractToEventSigToAction[contractAddress][sighash], f) { 78 | ContractToEventSigToAction[contractAddress][sighash] = append(ContractToEventSigToAction[contractAddress][sighash], f) 79 | } else { 80 | shared.Infof(slog.Default(), "addAddressEventSigAction - ignoring since already in array") 81 | } 82 | 83 | // add contract to filter addresses if not already added 84 | if !slices.Contains(shared.Options.FilterAddresses, contractAddress) { 85 | shared.Options.FilterAddresses = append(shared.Options.FilterAddresses, contractAddress) 86 | } 87 | 88 | // add event sig to filter topics if not already added 89 | if len(shared.Options.FilterEventTopics) == 0 { 90 | shared.Options.FilterEventTopics = [][]common.Hash{{}} 91 | } 92 | if !slices.Contains(shared.Options.FilterEventTopics[0], sighash) { 93 | shared.Options.FilterEventTopics[0] = append(shared.Options.FilterEventTopics[0], sighash) 94 | } 95 | 96 | ActionMapMutex.Unlock() 97 | } 98 | 99 | // not to be confused with addAddressEventSigAction 100 | // this function adds mapping for contract -> event function 101 | // it sends all events to the function, regardless of event sig 102 | func addAddressEventAction(contractAddress common.Address, f func(ActionEventData)) { 103 | ActionMapMutex.Lock() 104 | // make sure action func is not already in array 105 | if !containsActionEventFunc(ContractToEventAction[contractAddress], f) { 106 | // add to map 107 | ContractToEventAction[contractAddress] = append(ContractToEventAction[contractAddress], f) 108 | } else { 109 | shared.Infof(slog.Default(), "addAddressEventAction - ignoring since already in array") 110 | } 111 | 112 | ActionMapMutex.Unlock() 113 | } 114 | 115 | // add an action function for transactions, from/to address -> function 116 | func addTxAddressAction(address common.Address, f func(ActionTxData)) { 117 | ActionMapMutex.Lock() 118 | // make sure action func is not already in array 119 | if !containsActionTxFunc(TxAddressToAction[address], f) { 120 | // add to map 121 | TxAddressToAction[address] = append(TxAddressToAction[address], f) 122 | } else { 123 | shared.Infof(slog.Default(), "addTxAddressAction - ignoring since already in array") 124 | } 125 | ActionMapMutex.Unlock() 126 | } 127 | 128 | // add an action function for blocks 129 | func addBlockAction(f func(ActionBlockData)) { 130 | // make sure action func is not already in array 131 | if !containsActionBlockFunc(BlockActions, f) { 132 | BlockActions = append(BlockActions, f) 133 | } else { 134 | shared.Infof(slog.Default(), "addBlockAction - ignoring since already in array") 135 | } 136 | } 137 | 138 | // helpers 139 | 140 | // helpers to check whether or not array of funcs contains a func 141 | // (yes, 3 separate funcs for each data type. bad, but otherwise more reflection is required which is worse) 142 | 143 | func containsActionEventFunc(funcs []func(ActionEventData), target func(ActionEventData)) bool { 144 | for _, f := range funcs { 145 | if reflect.ValueOf(f).Pointer() == reflect.ValueOf(target).Pointer() { 146 | return true 147 | } 148 | } 149 | return false 150 | } 151 | 152 | func containsActionTxFunc(funcs []func(ActionTxData), target func(ActionTxData)) bool { 153 | for _, f := range funcs { 154 | if reflect.ValueOf(f).Pointer() == reflect.ValueOf(target).Pointer() { 155 | return true 156 | } 157 | } 158 | return false 159 | } 160 | 161 | func containsActionBlockFunc(funcs []func(ActionBlockData), target func(ActionBlockData)) bool { 162 | for _, f := range funcs { 163 | if reflect.ValueOf(f).Pointer() == reflect.ValueOf(target).Pointer() { 164 | return true 165 | } 166 | } 167 | return false 168 | } 169 | -------------------------------------------------------------------------------- /actions/init.go: -------------------------------------------------------------------------------- 1 | package actions 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "log/slog" 7 | "math/big" 8 | "reflect" 9 | "slices" 10 | "strings" 11 | "sync" 12 | "sync/atomic" 13 | 14 | "github.com/Zellic/EVM-trackooor/utils" 15 | 16 | "github.com/Zellic/EVM-trackooor/shared" 17 | 18 | "github.com/ethereum/go-ethereum/common" 19 | "github.com/ethereum/go-ethereum/core/types" 20 | ) 21 | 22 | type ActionEventData struct { 23 | EventLog types.Log 24 | EventFields shared.EventFields 25 | DecodedTopics map[string]interface{} 26 | DecodedData map[string]interface{} 27 | } 28 | 29 | type ActionTxData struct { 30 | Transaction *types.Transaction 31 | From *common.Address 32 | To *common.Address 33 | Block *types.Block // block which the tx was in 34 | } 35 | 36 | type ActionBlockData struct { 37 | Block *types.Block 38 | } 39 | 40 | // map event sig hash to action functions 41 | var EventSigToAction map[common.Hash][]func(ActionEventData) 42 | 43 | // map contract address and event sig to functions 44 | var ContractToEventSigToAction map[common.Address]map[common.Hash][]func(ActionEventData) 45 | 46 | // map contract address to event handling action function 47 | var ContractToEventAction map[common.Address][]func(ActionEventData) 48 | 49 | // map transaction `to` or `from` address to action functions 50 | var TxAddressToAction map[common.Address][]func(ActionTxData) 51 | 52 | // array of action functions, all of which are called when block is mined 53 | var BlockActions []func(ActionBlockData) 54 | 55 | type WaitGroupCount struct { 56 | sync.WaitGroup 57 | count int64 58 | } 59 | 60 | func (wg *WaitGroupCount) Add(delta int) { 61 | atomic.AddInt64(&wg.count, int64(delta)) 62 | wg.WaitGroup.Add(delta) 63 | } 64 | 65 | func (wg *WaitGroupCount) Done() { 66 | atomic.AddInt64(&wg.count, -1) 67 | wg.WaitGroup.Done() 68 | } 69 | 70 | func (wg *WaitGroupCount) GetCount() int { 71 | return int(atomic.LoadInt64(&wg.count)) 72 | } 73 | 74 | // wait group used to track and wait for rpc requests 75 | // if RpcWaitGroup.GetCount() is too many, historical events listener calls RpcWaitGroup.Wait() 76 | var RpcWaitGroup WaitGroupCount 77 | 78 | func init() { 79 | // init maps 80 | EventSigToAction = make(map[common.Hash][]func(ActionEventData)) 81 | ContractToEventSigToAction = make(map[common.Address]map[common.Hash][]func(ActionEventData)) 82 | ContractToEventAction = make(map[common.Address][]func(ActionEventData)) 83 | TxAddressToAction = make(map[common.Address][]func(ActionTxData)) 84 | 85 | shared.CachedBlocks = make(map[*big.Int]*types.Header) 86 | } 87 | 88 | // initializes all actions 89 | // by calling all method functions of action struct 90 | // that are enabled in config file 91 | func InitActions(actions map[string]shared.ActionOptions) { 92 | // list of all methods of actionInit 93 | var actionNames []string 94 | 95 | Struct := action{} 96 | StructType := reflect.TypeOf(Struct) 97 | // loop through all methods of actionInit 98 | for i := 0; i < StructType.NumMethod(); i++ { 99 | method := StructType.Method(i) 100 | actionName := utils.TrimFirstInstance(method.Name, "Init") 101 | 102 | if strings.HasPrefix(method.Name, "Init") { 103 | // add to action names if is a init function 104 | actionNames = append(actionNames, actionName) 105 | 106 | if actionOptions, ok := actions[actionName]; ok && // its name exists in the Actions -> ActionOptions map 107 | strings.HasPrefix(method.Name, "Init") && // func name begins with Init 108 | method.PkgPath == "" { // check if method is exported (uppercase) 109 | // call action init func with options 110 | fmt.Printf("Initializing action: %v\n", method.Name) 111 | // shared.Infof(slog.Default(), "PostProcessing init: calling %v()\n", method.Name) 112 | method.Func.Call([]reflect.Value{reflect.ValueOf(action{ 113 | o: actionOptions, 114 | }, 115 | )}) 116 | } 117 | } 118 | 119 | } 120 | 121 | // make sure all actions in config actually exist 122 | // by looping through all actions in config, and making sure a method exists 123 | for actionName := range actions { 124 | if !slices.Contains(actionNames, actionName) { 125 | log.Fatalf("Action '%v' does not exist, but was specified in config!", actionName) 126 | } 127 | } 128 | 129 | // determine whether or not we track blocks or events 130 | if len(TxAddressToAction) == 0 && len(BlockActions) == 0 { 131 | fmt.Printf("Auto determined to only track events\n") 132 | shared.BlockTrackingRequired = false 133 | } else { 134 | fmt.Printf("Auto determined to track blocks\n") 135 | shared.BlockTrackingRequired = true 136 | } 137 | } 138 | 139 | // call all action finish functions 140 | // only called by historical tracking after historical finishes 141 | func FinishActions(actions map[string]shared.ActionOptions) { 142 | Struct := action{} 143 | StructType := reflect.TypeOf(Struct) 144 | // loop through all methods of actionInit 145 | for i := 0; i < StructType.NumMethod(); i++ { 146 | method := StructType.Method(i) 147 | // check if enabled in config 148 | actionName := utils.TrimFirstInstance(method.Name, "Finished") 149 | if actionOptions, ok := actions[actionName]; ok && // its name exists in the actionOptions map 150 | strings.HasPrefix(method.Name, "Finished") && // func name begins with Init 151 | method.PkgPath == "" { // check if method is exported (uppercase) 152 | // call action finished func with options 153 | shared.Infof(slog.Default(), "Action finished: calling %v()\n", method.Name) 154 | method.Func.Call([]reflect.Value{reflect.ValueOf(action{ 155 | o: actionOptions, 156 | }, 157 | )}) 158 | } 159 | } 160 | } 161 | 162 | // display info about all actions 163 | 164 | const ( 165 | Reset = "\033[0m" 166 | Bold = "\033[1m" 167 | Underline = "\033[4m" 168 | Italic = "\033[3m" 169 | ) 170 | 171 | func DisplayActions() { 172 | Struct := actionInfo{} 173 | StructType := reflect.TypeOf(Struct) 174 | // loop through all methods of actionInfo 175 | for i := 0; i < StructType.NumMethod(); i++ { 176 | method := StructType.Method(i) 177 | if strings.HasPrefix(method.Name, "Info") && // func name begins with Info 178 | method.PkgPath == "" { // check if method is exported (uppercase) 179 | // retrieve action info 180 | reflectedValues := method.Func.Call([]reflect.Value{reflect.ValueOf(actionInfo{})}) 181 | reflectedStruct := reflectedValues[0] 182 | // unpack into struct 183 | var info actionInfo 184 | structValue := reflect.ValueOf(&info).Elem() 185 | for i := 0; i < reflectedStruct.NumField(); i++ { 186 | fieldName := reflectedStruct.Type().Field(i).Name 187 | fieldValue := reflectedStruct.Field(i) 188 | actionField := structValue.FieldByName(fieldName) 189 | 190 | if actionField.IsValid() && actionField.CanSet() { 191 | actionField.Set(fieldValue) 192 | } 193 | } 194 | // display info 195 | fmt.Printf( 196 | Underline+Bold+"\n%v\n"+Reset+ 197 | "%v\n", 198 | info.ActionName, 199 | info.ActionOverview, 200 | ) 201 | } 202 | } 203 | fmt.Printf(Italic + "\nRun actions for more info about specific actions." + Reset) 204 | } 205 | 206 | // display full action info (including options) of actions that contain the given action name substring 207 | // case insensitive 208 | func DisplayActionInfo(actionNameSubstr string) { 209 | Struct := actionInfo{} 210 | StructType := reflect.TypeOf(Struct) 211 | 212 | found := false 213 | // loop through all methods of actionInfo 214 | for i := 0; i < StructType.NumMethod(); i++ { 215 | method := StructType.Method(i) 216 | if strings.HasPrefix(method.Name, "Info") && // func name begins with Info 217 | method.PkgPath == "" { // check if method is exported (uppercase) 218 | // retrieve action info 219 | reflectedValues := method.Func.Call([]reflect.Value{reflect.ValueOf(actionInfo{})}) 220 | reflectedStruct := reflectedValues[0] 221 | // unpack into struct 222 | var info actionInfo 223 | structValue := reflect.ValueOf(&info).Elem() 224 | for i := 0; i < reflectedStruct.NumField(); i++ { 225 | fieldName := reflectedStruct.Type().Field(i).Name 226 | fieldValue := reflectedStruct.Field(i) 227 | actionField := structValue.FieldByName(fieldName) 228 | 229 | if actionField.IsValid() && actionField.CanSet() { 230 | actionField.Set(fieldValue) 231 | } 232 | } 233 | // display info if name matches 234 | if strings.Contains(strings.ToLower(info.ActionName), strings.ToLower(actionNameSubstr)) { 235 | fmt.Printf( 236 | Underline+Bold+"\n%v\n"+Reset+ 237 | "%v\n"+ 238 | "%v\n"+ 239 | Bold+"\nOptions:\n"+Reset+ 240 | "%v\n"+ 241 | Bold+"\nExample config:\n"+Reset+ 242 | "%v\n", 243 | info.ActionName, 244 | info.ActionOverview, 245 | info.ActionDescription, 246 | info.ActionOptionDetails, 247 | info.ActionConfigExample, 248 | ) 249 | found = true 250 | } 251 | } 252 | } 253 | if !found { 254 | fmt.Printf("No actions with descriptions found for '%v', maybe this action doesn't have a description written?\n", actionNameSubstr) 255 | } 256 | } 257 | -------------------------------------------------------------------------------- /contracts/IERC20/IERC20.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // OpenZeppelin Contracts (last updated v5.0.0) (token/ERC20/IERC20.sol) 3 | 4 | pragma solidity ^0.8.20; 5 | 6 | /** 7 | * @dev Interface of the ERC-20 standard as defined in the ERC. 8 | */ 9 | interface IERC20 { 10 | /** 11 | * @dev Emitted when `value` tokens are moved from one account (`from`) to 12 | * another (`to`). 13 | * 14 | * Note that `value` may be zero. 15 | */ 16 | event Transfer(address indexed from, address indexed to, uint256 value); 17 | 18 | /** 19 | * @dev Emitted when the allowance of a `spender` for an `owner` is set by 20 | * a call to {approve}. `value` is the new allowance. 21 | */ 22 | event Approval(address indexed owner, address indexed spender, uint256 value); 23 | 24 | /** 25 | * @dev Returns the value of tokens in existence. 26 | */ 27 | function totalSupply() external view returns (uint256); 28 | 29 | /** 30 | * @dev Returns the value of tokens owned by `account`. 31 | */ 32 | function balanceOf(address account) external view returns (uint256); 33 | 34 | /** 35 | * @dev Moves a `value` amount of tokens from the caller's account to `to`. 36 | * 37 | * Returns a boolean value indicating whether the operation succeeded. 38 | * 39 | * Emits a {Transfer} event. 40 | */ 41 | function transfer(address to, uint256 value) external returns (bool); 42 | 43 | /** 44 | * @dev Returns the remaining number of tokens that `spender` will be 45 | * allowed to spend on behalf of `owner` through {transferFrom}. This is 46 | * zero by default. 47 | * 48 | * This value changes when {approve} or {transferFrom} are called. 49 | */ 50 | function allowance(address owner, address spender) external view returns (uint256); 51 | 52 | /** 53 | * @dev Sets a `value` amount of tokens as the allowance of `spender` over the 54 | * caller's tokens. 55 | * 56 | * Returns a boolean value indicating whether the operation succeeded. 57 | * 58 | * IMPORTANT: Beware that changing an allowance with this method brings the risk 59 | * that someone may use both the old and the new allowance by unfortunate 60 | * transaction ordering. One possible solution to mitigate this race 61 | * condition is to first reduce the spender's allowance to 0 and set the 62 | * desired value afterwards: 63 | * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 64 | * 65 | * Emits an {Approval} event. 66 | */ 67 | function approve(address spender, uint256 value) external returns (bool); 68 | 69 | /** 70 | * @dev Moves a `value` amount of tokens from `from` to `to` using the 71 | * allowance mechanism. `value` is then deducted from the caller's 72 | * allowance. 73 | * 74 | * Returns a boolean value indicating whether the operation succeeded. 75 | * 76 | * Emits a {Transfer} event. 77 | */ 78 | function transferFrom(address from, address to, uint256 value) external returns (bool); 79 | } -------------------------------------------------------------------------------- /contracts/IERC20/abi/IERC20.abi: -------------------------------------------------------------------------------- 1 | [{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"spender","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"from","type":"address"},{"indexed":true,"internalType":"address","name":"to","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"}],"name":"Transfer","type":"event"},{"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"address","name":"spender","type":"address"}],"name":"allowance","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"}],"name":"approve","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"account","type":"address"}],"name":"balanceOf","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"totalSupply","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"}],"name":"transfer","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"}],"name":"transferFrom","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"}] -------------------------------------------------------------------------------- /contracts/IERC20Metadata/IERC20Metadata.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // OpenZeppelin Contracts (last updated v5.0.0) (token/ERC20/extensions/IERC20Metadata.sol) 3 | 4 | pragma solidity ^0.8.20; 5 | 6 | import {IERC20} from "../IERC20/IERC20.sol"; 7 | 8 | /** 9 | * @dev Interface for the optional metadata functions from the ERC-20 standard. 10 | */ 11 | interface IERC20Metadata is IERC20 { 12 | /** 13 | * @dev Returns the name of the token. 14 | */ 15 | function name() external view returns (string memory); 16 | 17 | /** 18 | * @dev Returns the symbol of the token. 19 | */ 20 | function symbol() external view returns (string memory); 21 | 22 | /** 23 | * @dev Returns the decimals places of the token. 24 | */ 25 | function decimals() external view returns (uint8); 26 | } 27 | -------------------------------------------------------------------------------- /contracts/IERC20Metadata/abi/IERC20.abi: -------------------------------------------------------------------------------- 1 | [{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"spender","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"from","type":"address"},{"indexed":true,"internalType":"address","name":"to","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"}],"name":"Transfer","type":"event"},{"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"address","name":"spender","type":"address"}],"name":"allowance","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"}],"name":"approve","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"account","type":"address"}],"name":"balanceOf","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"totalSupply","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"}],"name":"transfer","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"}],"name":"transferFrom","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"}] -------------------------------------------------------------------------------- /contracts/IERC20Metadata/abi/IERC20Metadata.abi: -------------------------------------------------------------------------------- 1 | [{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"spender","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"from","type":"address"},{"indexed":true,"internalType":"address","name":"to","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"}],"name":"Transfer","type":"event"},{"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"address","name":"spender","type":"address"}],"name":"allowance","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"}],"name":"approve","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"account","type":"address"}],"name":"balanceOf","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"decimals","outputs":[{"internalType":"uint8","name":"","type":"uint8"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"name","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"symbol","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"totalSupply","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"}],"name":"transfer","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"}],"name":"transferFrom","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"}] -------------------------------------------------------------------------------- /contracts/Multicall2/Multicall2.sol: -------------------------------------------------------------------------------- 1 | pragma solidity >=0.5.0; 2 | pragma experimental ABIEncoderV2; 3 | 4 | /// @title Multicall2 - Aggregate results from multiple read-only function calls 5 | /// @author Michael Elliot 6 | /// @author Joshua Levine 7 | /// @author Nick Johnson 8 | 9 | contract Multicall2 { 10 | struct Call { 11 | address target; 12 | bytes callData; 13 | } 14 | struct Result { 15 | bool success; 16 | bytes returnData; 17 | } 18 | 19 | function aggregate(Call[] memory calls) public returns (uint256 blockNumber, bytes[] memory returnData) { 20 | blockNumber = block.number; 21 | returnData = new bytes[](calls.length); 22 | for(uint256 i = 0; i < calls.length; i++) { 23 | (bool success, bytes memory ret) = calls[i].target.call(calls[i].callData); 24 | require(success, "Multicall aggregate: call failed"); 25 | returnData[i] = ret; 26 | } 27 | } 28 | function blockAndAggregate(Call[] memory calls) public returns (uint256 blockNumber, bytes32 blockHash, Result[] memory returnData) { 29 | (blockNumber, blockHash, returnData) = tryBlockAndAggregate(true, calls); 30 | } 31 | function getBlockHash(uint256 blockNumber) public view returns (bytes32 blockHash) { 32 | blockHash = blockhash(blockNumber); 33 | } 34 | function getBlockNumber() public view returns (uint256 blockNumber) { 35 | blockNumber = block.number; 36 | } 37 | function getCurrentBlockCoinbase() public view returns (address coinbase) { 38 | coinbase = block.coinbase; 39 | } 40 | function getCurrentBlockDifficulty() public view returns (uint256 difficulty) { 41 | difficulty = block.difficulty; 42 | } 43 | function getCurrentBlockGasLimit() public view returns (uint256 gaslimit) { 44 | gaslimit = block.gaslimit; 45 | } 46 | function getCurrentBlockTimestamp() public view returns (uint256 timestamp) { 47 | timestamp = block.timestamp; 48 | } 49 | function getEthBalance(address addr) public view returns (uint256 balance) { 50 | balance = addr.balance; 51 | } 52 | function getLastBlockHash() public view returns (bytes32 blockHash) { 53 | blockHash = blockhash(block.number - 1); 54 | } 55 | function tryAggregate(bool requireSuccess, Call[] memory calls) public returns (Result[] memory returnData) { 56 | returnData = new Result[](calls.length); 57 | for(uint256 i = 0; i < calls.length; i++) { 58 | (bool success, bytes memory ret) = calls[i].target.call(calls[i].callData); 59 | 60 | if (requireSuccess) { 61 | require(success, "Multicall2 aggregate: call failed"); 62 | } 63 | 64 | returnData[i] = Result(success, ret); 65 | } 66 | } 67 | function tryBlockAndAggregate(bool requireSuccess, Call[] memory calls) public returns (uint256 blockNumber, bytes32 blockHash, Result[] memory returnData) { 68 | blockNumber = block.number; 69 | blockHash = blockhash(block.number); 70 | returnData = tryAggregate(requireSuccess, calls); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /contracts/Multicall2/Multicall2_sol_Multicall2.abi: -------------------------------------------------------------------------------- 1 | [{"inputs":[{"components":[{"internalType":"address","name":"target","type":"address"},{"internalType":"bytes","name":"callData","type":"bytes"}],"internalType":"struct Multicall2.Call[]","name":"calls","type":"tuple[]"}],"name":"aggregate","outputs":[{"internalType":"uint256","name":"blockNumber","type":"uint256"},{"internalType":"bytes[]","name":"returnData","type":"bytes[]"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"components":[{"internalType":"address","name":"target","type":"address"},{"internalType":"bytes","name":"callData","type":"bytes"}],"internalType":"struct Multicall2.Call[]","name":"calls","type":"tuple[]"}],"name":"blockAndAggregate","outputs":[{"internalType":"uint256","name":"blockNumber","type":"uint256"},{"internalType":"bytes32","name":"blockHash","type":"bytes32"},{"components":[{"internalType":"bool","name":"success","type":"bool"},{"internalType":"bytes","name":"returnData","type":"bytes"}],"internalType":"struct Multicall2.Result[]","name":"returnData","type":"tuple[]"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"blockNumber","type":"uint256"}],"name":"getBlockHash","outputs":[{"internalType":"bytes32","name":"blockHash","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getBlockNumber","outputs":[{"internalType":"uint256","name":"blockNumber","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getCurrentBlockCoinbase","outputs":[{"internalType":"address","name":"coinbase","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getCurrentBlockDifficulty","outputs":[{"internalType":"uint256","name":"difficulty","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getCurrentBlockGasLimit","outputs":[{"internalType":"uint256","name":"gaslimit","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getCurrentBlockTimestamp","outputs":[{"internalType":"uint256","name":"timestamp","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"addr","type":"address"}],"name":"getEthBalance","outputs":[{"internalType":"uint256","name":"balance","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getLastBlockHash","outputs":[{"internalType":"bytes32","name":"blockHash","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bool","name":"requireSuccess","type":"bool"},{"components":[{"internalType":"address","name":"target","type":"address"},{"internalType":"bytes","name":"callData","type":"bytes"}],"internalType":"struct Multicall2.Call[]","name":"calls","type":"tuple[]"}],"name":"tryAggregate","outputs":[{"components":[{"internalType":"bool","name":"success","type":"bool"},{"internalType":"bytes","name":"returnData","type":"bytes"}],"internalType":"struct Multicall2.Result[]","name":"returnData","type":"tuple[]"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bool","name":"requireSuccess","type":"bool"},{"components":[{"internalType":"address","name":"target","type":"address"},{"internalType":"bytes","name":"callData","type":"bytes"}],"internalType":"struct Multicall2.Call[]","name":"calls","type":"tuple[]"}],"name":"tryBlockAndAggregate","outputs":[{"internalType":"uint256","name":"blockNumber","type":"uint256"},{"internalType":"bytes32","name":"blockHash","type":"bytes32"},{"components":[{"internalType":"bool","name":"success","type":"bool"},{"internalType":"bytes","name":"returnData","type":"bytes"}],"internalType":"struct Multicall2.Result[]","name":"returnData","type":"tuple[]"}],"stateMutability":"nonpayable","type":"function"}] -------------------------------------------------------------------------------- /contracts/Multicall2/Multicall2_sol_Multicall2.bin: -------------------------------------------------------------------------------- 1 | 608060405234801561001057600080fd5b506110e4806100206000396000f3fe608060405234801561001057600080fd5b50600436106100b45760003560e01c806372425d9d1161007157806372425d9d146101a657806386d516e8146101c4578063a8b0574e146101e2578063bce38bd714610200578063c3077fa914610230578063ee82ac5e14610262576100b4565b80630f28c97d146100b9578063252dba42146100d757806327e86d6e14610108578063399542e91461012657806342cbb15c146101585780634d2301cc14610176575b600080fd5b6100c1610292565b6040516100ce91906106a3565b60405180910390f35b6100f160048036038101906100ec91906109d2565b61029a565b6040516100ff929190610b5c565b60405180910390f35b610110610423565b60405161011d9190610ba5565b60405180910390f35b610140600480360381019061013b9190610bf8565b610438565b60405161014f93929190610d62565b60405180910390f35b610160610457565b60405161016d91906106a3565b60405180910390f35b610190600480360381019061018b9190610da0565b61045f565b60405161019d91906106a3565b60405180910390f35b6101ae610480565b6040516101bb91906106a3565b60405180910390f35b6101cc610488565b6040516101d991906106a3565b60405180910390f35b6101ea610490565b6040516101f79190610ddc565b60405180910390f35b61021a60048036038101906102159190610bf8565b610498565b6040516102279190610df7565b60405180910390f35b61024a600480360381019061024591906109d2565b610640565b60405161025993929190610d62565b60405180910390f35b61027c60048036038101906102779190610e45565b610663565b6040516102899190610ba5565b60405180910390f35b600042905090565b60006060439150825167ffffffffffffffff8111156102bc576102bb6106e8565b5b6040519080825280602002602001820160405280156102ef57816020015b60608152602001906001900390816102da5790505b50905060005b835181101561041d5760008085838151811061031457610313610e72565b5b60200260200101516000015173ffffffffffffffffffffffffffffffffffffffff1686848151811061034957610348610e72565b5b6020026020010151602001516040516103629190610edd565b6000604051808303816000865af19150503d806000811461039f576040519150601f19603f3d011682016040523d82523d6000602084013e6103a4565b606091505b5091509150816103e9576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016103e090610f51565b60405180910390fd5b808484815181106103fd576103fc610e72565b5b60200260200101819052505050808061041590610fa0565b9150506102f5565b50915091565b60006001436104329190610fe8565b40905090565b60008060604392504340915061044e8585610498565b90509250925092565b600043905090565b60008173ffffffffffffffffffffffffffffffffffffffff16319050919050565b600044905090565b600045905090565b600041905090565b6060815167ffffffffffffffff8111156104b5576104b46106e8565b5b6040519080825280602002602001820160405280156104ee57816020015b6104db61066e565b8152602001906001900390816104d35790505b50905060005b82518110156106395760008084838151811061051357610512610e72565b5b60200260200101516000015173ffffffffffffffffffffffffffffffffffffffff1685848151811061054857610547610e72565b5b6020026020010151602001516040516105619190610edd565b6000604051808303816000865af19150503d806000811461059e576040519150601f19603f3d011682016040523d82523d6000602084013e6105a3565b606091505b509150915085156105ef57816105ee576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016105e59061108e565b60405180910390fd5b5b604051806040016040528083151581526020018281525084848151811061061957610618610e72565b5b60200260200101819052505050808061063190610fa0565b9150506104f4565b5092915050565b6000806060610650600185610438565b8093508194508295505050509193909250565b600081409050919050565b6040518060400160405280600015158152602001606081525090565b6000819050919050565b61069d8161068a565b82525050565b60006020820190506106b86000830184610694565b92915050565b6000604051905090565b600080fd5b600080fd5b600080fd5b6000601f19601f8301169050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b610720826106d7565b810181811067ffffffffffffffff8211171561073f5761073e6106e8565b5b80604052505050565b60006107526106be565b905061075e8282610717565b919050565b600067ffffffffffffffff82111561077e5761077d6106e8565b5b602082029050602081019050919050565b600080fd5b600080fd5b600080fd5b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b60006107c98261079e565b9050919050565b6107d9816107be565b81146107e457600080fd5b50565b6000813590506107f6816107d0565b92915050565b600080fd5b600067ffffffffffffffff82111561081c5761081b6106e8565b5b610825826106d7565b9050602081019050919050565b82818337600083830152505050565b600061085461084f84610801565b610748565b9050828152602081018484840111156108705761086f6107fc565b5b61087b848285610832565b509392505050565b600082601f830112610898576108976106d2565b5b81356108a8848260208601610841565b91505092915050565b6000604082840312156108c7576108c6610794565b5b6108d16040610748565b905060006108e1848285016107e7565b600083015250602082013567ffffffffffffffff81111561090557610904610799565b5b61091184828501610883565b60208301525092915050565b600061093061092b84610763565b610748565b905080838252602082019050602084028301858111156109535761095261078f565b5b835b8181101561099a57803567ffffffffffffffff811115610978576109776106d2565b5b80860161098589826108b1565b85526020850194505050602081019050610955565b5050509392505050565b600082601f8301126109b9576109b86106d2565b5b81356109c984826020860161091d565b91505092915050565b6000602082840312156109e8576109e76106c8565b5b600082013567ffffffffffffffff811115610a0657610a056106cd565b5b610a12848285016109a4565b91505092915050565b600081519050919050565b600082825260208201905092915050565b6000819050602082019050919050565b600081519050919050565b600082825260208201905092915050565b60005b83811015610a81578082015181840152602081019050610a66565b60008484015250505050565b6000610a9882610a47565b610aa28185610a52565b9350610ab2818560208601610a63565b610abb816106d7565b840191505092915050565b6000610ad28383610a8d565b905092915050565b6000602082019050919050565b6000610af282610a1b565b610afc8185610a26565b935083602082028501610b0e85610a37565b8060005b85811015610b4a5784840389528151610b2b8582610ac6565b9450610b3683610ada565b925060208a01995050600181019050610b12565b50829750879550505050505092915050565b6000604082019050610b716000830185610694565b8181036020830152610b838184610ae7565b90509392505050565b6000819050919050565b610b9f81610b8c565b82525050565b6000602082019050610bba6000830184610b96565b92915050565b60008115159050919050565b610bd581610bc0565b8114610be057600080fd5b50565b600081359050610bf281610bcc565b92915050565b60008060408385031215610c0f57610c0e6106c8565b5b6000610c1d85828601610be3565b925050602083013567ffffffffffffffff811115610c3e57610c3d6106cd565b5b610c4a858286016109a4565b9150509250929050565b600081519050919050565b600082825260208201905092915050565b6000819050602082019050919050565b610c8981610bc0565b82525050565b6000604083016000830151610ca76000860182610c80565b5060208301518482036020860152610cbf8282610a8d565b9150508091505092915050565b6000610cd88383610c8f565b905092915050565b6000602082019050919050565b6000610cf882610c54565b610d028185610c5f565b935083602082028501610d1485610c70565b8060005b85811015610d505784840389528151610d318582610ccc565b9450610d3c83610ce0565b925060208a01995050600181019050610d18565b50829750879550505050505092915050565b6000606082019050610d776000830186610694565b610d846020830185610b96565b8181036040830152610d968184610ced565b9050949350505050565b600060208284031215610db657610db56106c8565b5b6000610dc4848285016107e7565b91505092915050565b610dd6816107be565b82525050565b6000602082019050610df16000830184610dcd565b92915050565b60006020820190508181036000830152610e118184610ced565b905092915050565b610e228161068a565b8114610e2d57600080fd5b50565b600081359050610e3f81610e19565b92915050565b600060208284031215610e5b57610e5a6106c8565b5b6000610e6984828501610e30565b91505092915050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b600081905092915050565b6000610eb782610a47565b610ec18185610ea1565b9350610ed1818560208601610a63565b80840191505092915050565b6000610ee98284610eac565b915081905092915050565b600082825260208201905092915050565b7f4d756c746963616c6c206167677265676174653a2063616c6c206661696c6564600082015250565b6000610f3b602083610ef4565b9150610f4682610f05565b602082019050919050565b60006020820190508181036000830152610f6a81610f2e565b9050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b6000610fab8261068a565b91507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8203610fdd57610fdc610f71565b5b600182019050919050565b6000610ff38261068a565b9150610ffe8361068a565b925082820390508181111561101657611015610f71565b5b92915050565b7f4d756c746963616c6c32206167677265676174653a2063616c6c206661696c6560008201527f6400000000000000000000000000000000000000000000000000000000000000602082015250565b6000611078602183610ef4565b91506110838261101c565b604082019050919050565b600060208201905081810360008301526110a78161106b565b905091905056fea264697066735822122065ff5d84ac56a1dac443b9572e48402e647fef9c9384a2b0b89d6d81c070854864736f6c63430008130033 -------------------------------------------------------------------------------- /contracts/UniswapV3Factory/code.abi: -------------------------------------------------------------------------------- 1 | [{"inputs":[],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint24","name":"fee","type":"uint24"},{"indexed":true,"internalType":"int24","name":"tickSpacing","type":"int24"}],"name":"FeeAmountEnabled","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"oldOwner","type":"address"},{"indexed":true,"internalType":"address","name":"newOwner","type":"address"}],"name":"OwnerChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"token0","type":"address"},{"indexed":true,"internalType":"address","name":"token1","type":"address"},{"indexed":true,"internalType":"uint24","name":"fee","type":"uint24"},{"indexed":false,"internalType":"int24","name":"tickSpacing","type":"int24"},{"indexed":false,"internalType":"address","name":"pool","type":"address"}],"name":"PoolCreated","type":"event"},{"inputs":[{"internalType":"address","name":"tokenA","type":"address"},{"internalType":"address","name":"tokenB","type":"address"},{"internalType":"uint24","name":"fee","type":"uint24"}],"name":"createPool","outputs":[{"internalType":"address","name":"pool","type":"address"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint24","name":"fee","type":"uint24"},{"internalType":"int24","name":"tickSpacing","type":"int24"}],"name":"enableFeeAmount","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint24","name":"","type":"uint24"}],"name":"feeAmountTickSpacing","outputs":[{"internalType":"int24","name":"","type":"int24"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"uint24","name":"","type":"uint24"}],"name":"getPool","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"owner","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"parameters","outputs":[{"internalType":"address","name":"factory","type":"address"},{"internalType":"address","name":"token0","type":"address"},{"internalType":"address","name":"token1","type":"address"},{"internalType":"uint24","name":"fee","type":"uint24"},{"internalType":"int24","name":"tickSpacing","type":"int24"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_owner","type":"address"}],"name":"setOwner","outputs":[],"stateMutability":"nonpayable","type":"function"}] -------------------------------------------------------------------------------- /data/blockscanners.json: -------------------------------------------------------------------------------- 1 | { 2 | "1": 3 | { 4 | "address": "https://etherscan.io/address/%v", 5 | "token": "https://etherscan.io/token/%v", 6 | "block": "https://etherscan.io/block/%v", 7 | "transaction": "https://etherscan.io/tx/%v", 8 | "contract-abi": "https://api.etherscan.io/api?module=contract&action=getabi&address=%v&format=raw" 9 | }, 10 | "10": 11 | { 12 | "address": "https://optimistic.etherscan.io/address/%v", 13 | "token": "https://optimistic.etherscan.io/token/%v", 14 | "block": "https://optimistic.etherscan.io/block/%v", 15 | "transaction": "https://optimistic.etherscan.io/tx/%v", 16 | "contract-abi": "https://api-optimistic.etherscan.io/api?module=contract&action=getabi&address=%v&format=raw" 17 | }, 18 | "8453": 19 | { 20 | "address": "https://basescan.org/address/%v", 21 | "token": "https://basescan.org/token/%v", 22 | "block": "https://basescan.org/block/%v", 23 | "transaction": "https://basescan.org/tx/%v", 24 | "contract-abi": "https://api.basescan.org/api?module=contract&action=getabi&address=%v&format=raw" 25 | }, 26 | "42161": 27 | { 28 | "address": "https://arbiscan.io/address/%v", 29 | "token": "https://arbiscan.io/token/%v", 30 | "block": "https://arbiscan.io/block/%v", 31 | "transaction": "https://arbiscan.io/tx/%v", 32 | "contract-abi": "https://api.arbiscan.io/api?module=contract&action=getabi&address=%v&format=raw" 33 | }, 34 | "81457": 35 | { 36 | "address": "https://blastscan.io/address/%v", 37 | "token": "https://blastscan.io/token/%v", 38 | "block": "https://blastscan.io/block/%v", 39 | "transaction": "https://blastscan.io/tx/%v", 40 | "contract-abi": "https://api.blastscan.io/api?module=contract&action=getabi&address=%v&format=raw" 41 | }, 42 | "11155111": 43 | { 44 | "address": "https://sepolia.etherscan.io/address/%v", 45 | "token": "https://sepolia.etherscan.io/token/%v", 46 | "block": "https://sepolia.etherscan.io/block/%v", 47 | "transaction": "https://sepolia.etherscan.io/tx/%v" 48 | } 49 | } -------------------------------------------------------------------------------- /database/eventdata.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "log" 8 | "log/slog" 9 | "math/big" 10 | "net/http" 11 | "os" 12 | "strings" 13 | "time" 14 | 15 | "github.com/Zellic/EVM-trackooor/shared" 16 | 17 | "github.com/ethereum/go-ethereum/accounts/abi" 18 | "github.com/ethereum/go-ethereum/common" 19 | "github.com/ethereum/go-ethereum/crypto" 20 | ) 21 | 22 | // adds event data and abi to event data file 23 | func AddEventsFromAbiFile(abiFilepath string, outputFilepath string) { 24 | 25 | // open and load ABI file 26 | shared.Infof(slog.Default(), "Reading %v for ABI", abiFilepath) 27 | abiStr, _ := os.ReadFile(abiFilepath) 28 | 29 | // add events to events data file 30 | AddEventsFromAbi(string(abiStr), outputFilepath) 31 | } 32 | 33 | func AddEventsFromAbi(contractAbiString string, outputFilepath string) { 34 | // the reason we take a string is that marshalling abi.ABI causes 35 | // keys to get uppercased (due to how go handles internal/external variables) 36 | // so by providing a string, we retain the JSON to extract event abi 37 | // from there afterwards 38 | // TODO(actually i think this can be fixed with `json` struct tags) 39 | 40 | // load events data file 41 | var eventSigs map[string]interface{} 42 | shared.LoadFromJson("./data/event_sigs.json", &eventSigs) 43 | 44 | // load ABI as JSON 45 | var abiJson []map[string]interface{} 46 | err := json.Unmarshal([]byte(contractAbiString), &abiJson) 47 | if err != nil { 48 | log.Fatal(err) 49 | } 50 | 51 | // load ABI as abi.ABI 52 | shared.Infof(slog.Default(), "Extracting events from ABI") 53 | contractAbi, _ := abi.JSON(strings.NewReader(string(contractAbiString))) 54 | 55 | for _, event := range contractAbi.Events { 56 | eventInfoMap := make(map[string]interface{}) 57 | eventSigHash := crypto.Keccak256([]byte(event.Sig)) 58 | eventSigHashHex := common.BytesToHash(eventSigHash) 59 | 60 | // get event ABI from JSON ABI (reasons) 61 | var eventAbiJson map[string]interface{} 62 | for _, item := range abiJson { 63 | name, ok := item["name"].(string) 64 | if !ok { 65 | continue 66 | } 67 | itemType := item["type"].(string) 68 | if name == event.Name && itemType == "event" { 69 | eventAbiJson = item 70 | break 71 | } 72 | } 73 | // error if didnt find (shouldnt happen) 74 | if len(eventAbiJson) == 0 { 75 | shared.Warnf(slog.Default(), "Could not find event name %v in JSON, skipping!", event.Name) 76 | continue 77 | } 78 | 79 | eventInfoMap["name"] = event.Name 80 | eventInfoMap["sig"] = event.Sig 81 | eventInfoMap["abi"] = eventAbiJson 82 | 83 | eventSigs[eventSigHashHex.Hex()] = eventInfoMap 84 | fmt.Printf("Added %v\n", event.Sig) 85 | } 86 | 87 | // write out to data file 88 | shared.Infof(slog.Default(), "Writing out to %v", outputFilepath) 89 | eventSigsBytes, _ := json.Marshal(eventSigs) 90 | os.WriteFile(outputFilepath, eventSigsBytes, 0644) 91 | log.Printf("Data wrote to file: %v", outputFilepath) 92 | } 93 | 94 | func GetAbiFromBlockscanner(blockscannerData map[string]interface{}, chainID *big.Int, contractAddress common.Address) (string, error) { 95 | shared.Infof(slog.Default(), "Getting contract %v ABI from blockscanner", contractAddress.Hex()) 96 | 97 | chain, ok := blockscannerData[chainID.String()] 98 | if !ok { 99 | return "", fmt.Errorf("could not find blockscanner data for chain %v", chainID.String()) 100 | } 101 | chainMap := chain.(map[string]interface{}) 102 | url, ok := chainMap["contract-abi"] 103 | if !ok { 104 | return "", fmt.Errorf("could not find blockscanner contract abi data for chain %v", chainID.String()) 105 | } 106 | urlStr := url.(string) 107 | urlStr = fmt.Sprintf(urlStr, contractAddress.Hex()) 108 | 109 | // fetch abi with http get 110 | resp, err := http.Get(urlStr) 111 | if err != nil { 112 | log.Fatalln(err) 113 | } 114 | body, err := io.ReadAll(resp.Body) 115 | if err != nil { 116 | log.Fatalln(err) 117 | } 118 | abiStr := string(body) 119 | if strings.Contains(abiStr, "Contract source code not verified") { 120 | return "", fmt.Errorf("contract source code not verified for %v", contractAddress.Hex()) 121 | } 122 | if strings.Contains(abiStr, "Max rate limit reached, please use API Key for higher rate limit") { 123 | // rate limited, trying again in 5 seconds 124 | shared.Infof(slog.Default(), "Rate limited, trying again in 5 seconds") 125 | time.Sleep(5 * time.Second) 126 | return GetAbiFromBlockscanner(blockscannerData, chainID, contractAddress) 127 | } 128 | if strings.Contains(abiStr, "Missing/Invalid API Key") { 129 | return "", fmt.Errorf("invalid API key: %v", abiStr) 130 | } 131 | return abiStr, nil 132 | } 133 | -------------------------------------------------------------------------------- /database/funcdata.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/Zellic/EVM-trackooor/shared" 7 | "log" 8 | "log/slog" 9 | "os" 10 | "strings" 11 | 12 | "github.com/ethereum/go-ethereum/accounts/abi" 13 | "github.com/ethereum/go-ethereum/common" 14 | "github.com/ethereum/go-ethereum/crypto" 15 | ) 16 | 17 | func AddFuncSigsFromAbiFile(abiFilepath string, outputFilepath string) { 18 | // open and load ABI file 19 | shared.Infof(slog.Default(), "Reading %v for ABI", abiFilepath) 20 | abiStr, _ := os.ReadFile(abiFilepath) 21 | 22 | // add events to events data file 23 | AddFuncSigsFromAbi(string(abiStr), outputFilepath) 24 | } 25 | 26 | func AddFuncSigsFromAbi(contractAbiString string, outputFilepath string) { 27 | // load tx data file 28 | var funcSigs map[string]interface{} 29 | shared.LoadFromJson("./data/func_sigs.json", &funcSigs) 30 | 31 | // load ABI as JSON 32 | var abiJson []map[string]interface{} 33 | err := json.Unmarshal([]byte(contractAbiString), &abiJson) 34 | if err != nil { 35 | log.Fatal(err) 36 | } 37 | 38 | shared.Infof(slog.Default(), "Extracting function signatures from ABI") 39 | contractAbi, _ := abi.JSON(strings.NewReader(string(contractAbiString))) 40 | for _, contractFunc := range contractAbi.Methods { 41 | funcSig := contractFunc.Sig 42 | funcSigHash := crypto.Keccak256([]byte(funcSig)) 43 | funcSig4ByteHex := common.BytesToHash(funcSigHash).Hex()[:2+2*4] 44 | 45 | var funcAbiJson map[string]interface{} 46 | // get ABI of function that has name 47 | for _, item := range abiJson { 48 | name, ok := item["name"].(string) 49 | if !ok { 50 | continue 51 | } 52 | itemType := item["type"].(string) 53 | if name == contractFunc.Name && itemType == "function" { 54 | funcAbiJson = item 55 | break 56 | } 57 | } 58 | 59 | // error if didnt find (shouldnt happen) 60 | if len(funcAbiJson) == 0 { 61 | shared.Warnf(slog.Default(), "Could not find func name %v in JSON, skipping!", contractFunc.Name) 62 | continue 63 | } 64 | 65 | funcInfoMap := make(map[string]interface{}) 66 | funcInfoMap["name"] = contractFunc.Name 67 | funcInfoMap["sig"] = contractFunc.Sig 68 | funcInfoMap["abi"] = funcAbiJson 69 | 70 | funcSigs[funcSig4ByteHex] = funcInfoMap 71 | fmt.Printf("Added %v - %v\n", funcSig4ByteHex, funcSig) 72 | } 73 | 74 | // write out to data file 75 | shared.Infof(slog.Default(), "Writing out to %v", outputFilepath) 76 | funcSigBytes, _ := json.Marshal(funcSigs) 77 | os.WriteFile(outputFilepath, funcSigBytes, 0644) 78 | log.Printf("Data wrote to file: %v", outputFilepath) 79 | } 80 | -------------------------------------------------------------------------------- /discord-webhook/discordwebhook.go: -------------------------------------------------------------------------------- 1 | package discordwebhook 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "io/ioutil" 9 | "mime/multipart" 10 | "net/http" 11 | "time" 12 | ) 13 | 14 | type Field struct { 15 | Name string `json:"name"` 16 | Value string `json:"value"` 17 | Inline bool `json:"inline"` 18 | } 19 | type Thumbnail struct { 20 | Url string `json:"url"` 21 | } 22 | type Footer struct { 23 | Text string `json:"text"` 24 | Icon_url string `json:"icon_url"` 25 | } 26 | type Embed struct { 27 | Title string `json:"title"` 28 | Url string `json:"url"` 29 | Description string `json:"description"` 30 | Color int `json:"color"` 31 | Thumbnail Thumbnail `json:"thumbnail"` 32 | Footer Footer `json:"footer"` 33 | Fields []Field `json:"fields"` 34 | Timestamp time.Time `json:"timestamp"` 35 | Author Author `json:"author"` 36 | } 37 | 38 | type Author struct { 39 | Name string `json:"name"` 40 | Icon_URL string `json:"icon_url"` 41 | Url string `json:"url"` 42 | } 43 | 44 | type Attachment struct { 45 | Id string `json:"id"` 46 | Description string `json:"description"` 47 | Filename string `json:"filename"` 48 | } 49 | type Hook struct { 50 | Username string `json:"username"` 51 | Avatar_url string `json:"avatar_url"` 52 | Content string `json:"content"` 53 | Embeds []Embed `json:"embeds"` 54 | Attachments []Attachment `json:"attachments"` 55 | } 56 | 57 | func ExecuteWebhook(link string, data []byte) error { 58 | 59 | req, err := http.NewRequest("POST", link, bytes.NewBuffer(data)) 60 | if err != nil { 61 | return err 62 | } 63 | req.Header.Set("Content-Type", "application/json; charset=UTF-8") 64 | client := &http.Client{} 65 | resp, err := client.Do(req) 66 | if err != nil { 67 | return err 68 | } 69 | defer resp.Body.Close() 70 | bodyText, err := ioutil.ReadAll(resp.Body) 71 | if err != nil { 72 | return err 73 | } 74 | if resp.StatusCode != 200 && resp.StatusCode != 204 { 75 | return errors.New(fmt.Sprintf("%s\n", bodyText)) 76 | } 77 | // TODO rate limiting already handled? 78 | if resp.StatusCode == 429 { 79 | time.Sleep(time.Second * 5) 80 | return ExecuteWebhook(link, data) 81 | } 82 | return err 83 | } 84 | 85 | func SendEmbed(link string, embeds Embed) error { 86 | hook := Hook{ 87 | Embeds: []Embed{embeds}, 88 | } 89 | payload, err := json.Marshal(hook) 90 | if err != nil { 91 | return err 92 | } 93 | err = ExecuteWebhook(link, payload) 94 | return err 95 | 96 | } 97 | 98 | func SendFileToWebhook(link string, data []byte, fileData []byte, filename string) error { 99 | var b bytes.Buffer 100 | w := multipart.NewWriter(&b) 101 | 102 | if fw, err := w.CreateFormField("payload_json"); err != nil { 103 | return err 104 | } else { 105 | if _, err = fw.Write(data); err != nil { 106 | return err 107 | } 108 | } 109 | 110 | // Add the file 111 | if fw, err := w.CreateFormFile("files[0]", filename); err != nil { 112 | return err 113 | } else { 114 | if _, err = fw.Write(fileData); err != nil { 115 | return err 116 | } 117 | } 118 | 119 | w.Close() 120 | 121 | // Make the POST request 122 | req, err := http.NewRequest("POST", link, &b) 123 | if err != nil { 124 | return err 125 | } 126 | req.Header.Set("Content-Type", w.FormDataContentType()) 127 | 128 | client := &http.Client{} 129 | resp, err := client.Do(req) 130 | if err != nil { 131 | return err 132 | } 133 | defer resp.Body.Close() 134 | 135 | if (resp.StatusCode != 200) && (resp.StatusCode != 204) { 136 | return fmt.Errorf("failed to send file %v. Status code: %d Response: %v", filename, resp.StatusCode, resp.Body) 137 | } 138 | 139 | return nil 140 | } 141 | -------------------------------------------------------------------------------- /docs/imgs/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zellic/EVM-trackooor/95c36128e99eb4aa8f5697b86328a6c5f3b452c7/docs/imgs/banner.png -------------------------------------------------------------------------------- /docs/imgs/discord_tornado.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zellic/EVM-trackooor/95c36128e99eb4aa8f5697b86328a6c5f3b452c7/docs/imgs/discord_tornado.png -------------------------------------------------------------------------------- /docs/imgs/discord_uniswap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zellic/EVM-trackooor/95c36128e99eb4aa8f5697b86328a6c5f3b452c7/docs/imgs/discord_uniswap.png -------------------------------------------------------------------------------- /docs/imgs/graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zellic/EVM-trackooor/95c36128e99eb4aa8f5697b86328a6c5f3b452c7/docs/imgs/graph.png -------------------------------------------------------------------------------- /docs/imgs/logging.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zellic/EVM-trackooor/95c36128e99eb4aa8f5697b86328a6c5f3b452c7/docs/imgs/logging.png -------------------------------------------------------------------------------- /docs/imgs/ownership_transfer_proxy_upgrade_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zellic/EVM-trackooor/95c36128e99eb4aa8f5697b86328a6c5f3b452c7/docs/imgs/ownership_transfer_proxy_upgrade_example.png -------------------------------------------------------------------------------- /example_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "rpcurl":"wss://...", 3 | "actions":{ 4 | "Logging":{ 5 | "addresses":{ 6 | "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48":{} 7 | }, 8 | "options":{ 9 | "log-events": true, 10 | "log-transactions": true, 11 | "log-blocks": true, 12 | "log-any-deployments": true, 13 | "determine-tx-type": false, 14 | "enable-terminal-logs": true, 15 | "enable-discord-logs": false, 16 | "discord-log-options": { 17 | "webhook-url": "...", 18 | "username": "evm trackooor", 19 | "avatar-url": "...", 20 | "buffer-webhook-messages": true, 21 | "retry-webhook-messages": false 22 | } 23 | }, 24 | "enabled":true 25 | }, 26 | "TornadoCash":{ 27 | "addresses":{ 28 | "0x910Cbd523D972eb0a6f4cAe4618aD62622b39DbF":{ 29 | "name":"Torando.Cash 10 ETH" 30 | } 31 | }, 32 | "options":{ 33 | "output-filepath":"./tornadoCash_out.txt" 34 | }, 35 | "enabled":true 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/Zellic/EVM-trackooor 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.23.4 6 | 7 | require ( 8 | github.com/dominikbraun/graph v0.23.0 9 | github.com/ethereum/go-ethereum v1.15.10 10 | github.com/jackc/pgx/v5 v5.7.4 11 | github.com/spf13/cobra v1.9.1 12 | gonum.org/v1/plot v0.16.0 13 | ) 14 | 15 | replace github.com/ethereum/go-ethereum v1.14.3 => github.com/TheSavageTeddy/go-ethereum v0.1.5 16 | 17 | require ( 18 | codeberg.org/go-fonts/liberation v0.5.0 // indirect 19 | codeberg.org/go-latex/latex v0.1.0 // indirect 20 | codeberg.org/go-pdf/fpdf v0.11.0 // indirect 21 | git.sr.ht/~sbinet/gg v0.6.0 // indirect 22 | github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b // indirect 23 | github.com/campoy/embedmd v1.0.0 // indirect 24 | github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a // indirect 25 | github.com/ethereum/go-verkle v0.2.2 // indirect 26 | github.com/go-fonts/liberation v0.3.3 // indirect 27 | github.com/go-latex/latex v0.0.0-20250304174226-2790903426af // indirect 28 | github.com/go-pdf/fpdf v0.9.0 // indirect 29 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect 30 | github.com/jackc/pgpassfile v1.0.0 // indirect 31 | github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect 32 | github.com/jackc/puddle/v2 v2.2.2 // indirect 33 | github.com/pmezard/go-difflib v1.0.0 // indirect 34 | github.com/yusufpapurcu/wmi v1.2.4 // indirect 35 | golang.org/x/image v0.26.0 // indirect 36 | golang.org/x/text v0.24.0 // indirect 37 | ) 38 | 39 | require ( 40 | github.com/Microsoft/go-winio v0.6.2 // indirect 41 | github.com/bits-and-blooms/bitset v1.22.0 // indirect 42 | github.com/btcsuite/btcd/btcec/v2 v2.3.4 // indirect 43 | github.com/consensys/bavard v0.1.30 // indirect 44 | github.com/consensys/gnark-crypto v0.17.0 // indirect 45 | github.com/crate-crypto/go-kzg-4844 v1.1.0 // indirect 46 | github.com/deckarep/golang-set/v2 v2.8.0 // indirect 47 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect 48 | github.com/ethereum/c-kzg-4844 v1.0.3 // indirect 49 | github.com/fsnotify/fsnotify v1.9.0 // indirect 50 | github.com/go-ole/go-ole v1.3.0 // indirect 51 | github.com/google/uuid v1.6.0 // indirect 52 | github.com/gorilla/websocket v1.5.3 // indirect 53 | github.com/holiman/uint256 v1.3.2 // indirect 54 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 55 | github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect 56 | github.com/mmcloughlin/addchain v0.4.0 // indirect 57 | github.com/rivo/uniseg v0.4.7 // indirect 58 | github.com/schollz/progressbar/v3 v3.18.0 59 | github.com/shirou/gopsutil v3.21.11+incompatible // indirect 60 | github.com/spf13/pflag v1.0.6 // indirect 61 | github.com/supranational/blst v0.3.14 // indirect 62 | github.com/tklauser/go-sysconf v0.3.15 // indirect 63 | github.com/tklauser/numcpus v0.10.0 // indirect 64 | golang.org/x/crypto v0.37.0 // indirect 65 | golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect 66 | golang.org/x/sync v0.13.0 // indirect 67 | golang.org/x/sys v0.32.0 // indirect 68 | golang.org/x/term v0.31.0 // indirect 69 | rsc.io/tmplfunc v0.0.3 // indirect 70 | ) 71 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/Zellic/EVM-trackooor/actions" 7 | "github.com/Zellic/EVM-trackooor/database" 8 | "github.com/Zellic/EVM-trackooor/shared" 9 | "github.com/Zellic/EVM-trackooor/trackooor" 10 | 11 | "github.com/Zellic/EVM-trackooor/utils" 12 | 13 | "fmt" 14 | "log" 15 | "log/slog" 16 | "math/big" 17 | "os" 18 | "strings" 19 | 20 | "github.com/ethereum/go-ethereum/common" 21 | "github.com/ethereum/go-ethereum/crypto" 22 | "github.com/spf13/cobra" 23 | ) 24 | 25 | // GLOBAL VARIABLES 26 | 27 | // 28 | // command args/flags 29 | // 30 | 31 | // general 32 | var verbose bool 33 | 34 | // trackooors 35 | var configFilepath string // path to config file instead of specifying params on command line 36 | var AutoFetchABI bool 37 | var ContinueToRealtime bool 38 | var PendingBlocks bool 39 | var GetBlockByNumber bool 40 | 41 | // historical 42 | var fromBlockStr string 43 | var toBlockStr string 44 | var stepBlocksStr string 45 | 46 | var batchFetchBlocks bool 47 | 48 | // data 49 | var abiFilepath string 50 | var chainIdStr string 51 | var dataContractAddresses string 52 | 53 | var options shared.TrackooorOptions 54 | 55 | var rootCmd = &cobra.Command{ 56 | Use: "evm-trackooor", 57 | Short: "A modular tool to track anything on the EVM chain, including real-time tracking and alerts.", 58 | Long: `A modular tool to track anything on the EVM chain, including real-time tracking and alerts.`, 59 | Args: cobra.ExactArgs(1), 60 | PreRun: toggleVerbosity, 61 | Run: func(cmd *cobra.Command, args []string) {}, 62 | } 63 | 64 | var trackCmd = &cobra.Command{ 65 | Use: "track", 66 | Short: "Track events, transactions and blocks in realtime, or fetch historically", 67 | Long: ``, 68 | Args: cobra.ExactArgs(1), 69 | PreRun: toggleVerbosity, 70 | Run: func(cmd *cobra.Command, args []string) {}, 71 | } 72 | 73 | var realtimeCmd = &cobra.Command{ 74 | Use: "realtime", 75 | Short: "Track events, transactions and blocks in realtime", 76 | Long: `Track events, transactions and blocks in realtime`, 77 | Args: cobra.RangeArgs(0, 1), 78 | PreRun: toggleVerbosity, 79 | Run: func(cmd *cobra.Command, args []string) { 80 | setListenerOptions() 81 | loadConfigFile(configFilepath) 82 | 83 | trackooor.SetupListener(options) 84 | actions.InitActions(options.Actions) 85 | 86 | if shared.BlockTrackingRequired { 87 | trackooor.ListenToBlocks() 88 | } else { 89 | trackooor.ListenToEventsSingle() 90 | } 91 | }, 92 | } 93 | 94 | var realtimeEventsCmd = &cobra.Command{ 95 | Use: "events", 96 | Short: "Track events only in realtime, through a filter log subscription", 97 | Long: `Track events only in realtime, through a filter log subscription`, 98 | Args: cobra.ExactArgs(0), 99 | PreRun: toggleVerbosity, 100 | Run: func(cmd *cobra.Command, args []string) { 101 | setListenerOptions() 102 | 103 | loadConfigFile(configFilepath) 104 | 105 | trackooor.SetupListener(options) 106 | 107 | actions.InitActions(options.Actions) 108 | 109 | trackooor.ListenToEventsSingle() 110 | }, 111 | } 112 | 113 | var realtimeBlocksCmd = &cobra.Command{ 114 | Use: "blocks", 115 | Short: "Listens for newly mined blocks, to track events, transactions and blocks mined", 116 | Long: `Listens for newly mined blocks, to track events, transactions and blocks mined`, 117 | Args: cobra.ExactArgs(0), 118 | PreRun: toggleVerbosity, 119 | Run: func(cmd *cobra.Command, args []string) { 120 | setListenerOptions() 121 | 122 | loadConfigFile(configFilepath) 123 | 124 | trackooor.SetupListener(options) 125 | 126 | actions.InitActions(options.Actions) 127 | 128 | if PendingBlocks { 129 | trackooor.ListenToPendingBlocks() 130 | } else { 131 | 132 | trackooor.ListenToBlocks() 133 | } 134 | }, 135 | } 136 | 137 | var historicalCmd = &cobra.Command{ 138 | Use: "historical", 139 | Short: "Fetches historical events/transactions/blocks in a given block range", 140 | Long: `Fetches historical events/transactions/blocks in a given block range`, 141 | Args: cobra.RangeArgs(0, 1), 142 | PreRun: toggleVerbosity, 143 | Run: func(cmd *cobra.Command, args []string) { 144 | setListenerOptions() 145 | 146 | loadConfigFile(configFilepath) 147 | 148 | trackooor.SetupListener(options) 149 | trackooor.SetupHistorical() 150 | 151 | actions.InitActions(options.Actions) 152 | 153 | if shared.BlockTrackingRequired { 154 | trackooor.GetPastBlocks() 155 | } else { 156 | trackooor.GetPastEventsSingle() 157 | } 158 | }, 159 | } 160 | 161 | var historicalEventsCmd = &cobra.Command{ 162 | Use: "events", 163 | Short: "Fetches historically emitted events only in a given block range", 164 | Long: `Fetches historically emitted events only in a given block range`, 165 | Args: cobra.ExactArgs(0), 166 | PreRun: toggleVerbosity, 167 | Run: func(cmd *cobra.Command, args []string) { 168 | setListenerOptions() 169 | 170 | loadConfigFile(configFilepath) 171 | 172 | trackooor.SetupListener(options) 173 | trackooor.SetupHistorical() 174 | 175 | actions.InitActions(options.Actions) 176 | 177 | trackooor.GetPastEventsSingle() 178 | }, 179 | } 180 | 181 | var historicalBlocksCmd = &cobra.Command{ 182 | Use: "blocks", 183 | Short: "Fetches historical blocks in a given block range", 184 | Long: `Fetches historical blocks to process events and transactions in a given block range`, 185 | Args: cobra.ExactArgs(0), 186 | PreRun: toggleVerbosity, 187 | Run: func(cmd *cobra.Command, args []string) { 188 | setListenerOptions() 189 | 190 | loadConfigFile(configFilepath) 191 | 192 | trackooor.SetupListener(options) 193 | trackooor.SetupHistorical() 194 | 195 | actions.InitActions(options.Actions) 196 | 197 | trackooor.GetPastBlocks() 198 | }, 199 | } 200 | 201 | var dataCmd = &cobra.Command{ 202 | Use: "data", 203 | Short: "Add or modify data files such as event sigs and blockscanners", 204 | Long: `Add or modify data files such as event sigs and blockscanners`, 205 | Args: cobra.ExactArgs(1), 206 | PreRun: toggleVerbosity, 207 | Run: func(cmd *cobra.Command, args []string) { 208 | }, 209 | } 210 | 211 | var addEventsCmd = &cobra.Command{ 212 | Use: "event", 213 | Short: "Add event signatures to JSON data file from ABI file", 214 | Long: `Add event signatures to JSON data file from ABI file`, 215 | Args: cobra.ExactArgs(0), 216 | PreRun: toggleVerbosity, 217 | Run: func(cmd *cobra.Command, args []string) { 218 | outputFilepath := "./data/event_sigs.json" 219 | 220 | // if abi file provided, use that for abi data 221 | if abiFilepath != "" { 222 | database.AddEventsFromAbiFile(abiFilepath, outputFilepath) 223 | } else { 224 | // otherwise contract address was provided, use that 225 | 226 | // get blockscanner data 227 | var blockscannerData map[string]interface{} 228 | shared.LoadFromJson("./data/blockscanners.json", &blockscannerData) 229 | 230 | // convert chainID to big int 231 | chainId := big.NewInt(0) 232 | chainId, ok := chainId.SetString(chainIdStr, 10) 233 | if !ok { 234 | log.Fatalf("Could not convert %v to big.Int", chainId) 235 | } 236 | 237 | // get contract addresses 238 | contractAddresses := strings.Split(dataContractAddresses, ",") 239 | // get each contract's abi string 240 | var abiStrings []string 241 | for _, hexAddr := range contractAddresses { 242 | if !common.IsHexAddress(hexAddr) { 243 | log.Fatalf("Provided address %v is not a hex address", hexAddr) 244 | } 245 | abiStr, err := database.GetAbiFromBlockscanner( 246 | blockscannerData, 247 | chainId, 248 | common.HexToAddress(hexAddr), 249 | ) 250 | if err != nil { 251 | log.Fatal(err) 252 | } 253 | abiStrings = append(abiStrings, abiStr) 254 | } 255 | // add each abi string to output file 256 | for _, abiString := range abiStrings { 257 | database.AddEventsFromAbi(abiString, outputFilepath) 258 | } 259 | } 260 | }, 261 | } 262 | 263 | var addFuncSigCmd = &cobra.Command{ 264 | Use: "func", 265 | Short: "Add function signatures to JSON data file from ABI file", 266 | Long: `Add function signatures to JSON data file from ABI file`, 267 | Args: cobra.ExactArgs(0), 268 | PreRun: toggleVerbosity, 269 | Run: func(cmd *cobra.Command, args []string) { 270 | outputFilepath := "./data/func_sigs.json" 271 | 272 | // if abi file provided, use that for abi data 273 | if abiFilepath != "" { 274 | database.AddFuncSigsFromAbiFile(abiFilepath, outputFilepath) 275 | } else { 276 | // otherwise contract address was provided, use that 277 | 278 | // get blockscanner data 279 | var blockscannerData map[string]interface{} 280 | shared.LoadFromJson("./data/blockscanners.json", &blockscannerData) 281 | 282 | // convert chainID to big int 283 | chainId := big.NewInt(0) 284 | chainId, ok := chainId.SetString(chainIdStr, 10) 285 | if !ok { 286 | log.Fatalf("Could not convert %v to big.Int", chainId) 287 | } 288 | 289 | // get contract addresses 290 | contractAddresses := strings.Split(dataContractAddresses, ",") 291 | // get each contract's abi string 292 | var abiStrings []string 293 | for _, hexAddr := range contractAddresses { 294 | if !common.IsHexAddress(hexAddr) { 295 | log.Fatalf("Provided address %v is not a hex address", hexAddr) 296 | } 297 | abiStr, err := database.GetAbiFromBlockscanner( 298 | blockscannerData, 299 | chainId, 300 | common.HexToAddress(hexAddr), 301 | ) 302 | if err != nil { 303 | log.Fatal(err) 304 | } 305 | abiStrings = append(abiStrings, abiStr) 306 | } 307 | // add each abi string to output file 308 | for _, abiString := range abiStrings { 309 | database.AddFuncSigsFromAbi(abiString, outputFilepath) 310 | } 311 | } 312 | }, 313 | } 314 | 315 | var displayActionsCmd = &cobra.Command{ 316 | Use: "actions", 317 | Short: "Displays available actions (post processors) and their usage", 318 | Long: `Displays available actions (post processors) and their usage`, 319 | Args: cobra.RangeArgs(0, 1), 320 | PreRun: toggleVerbosity, 321 | Run: func(cmd *cobra.Command, args []string) { 322 | if len(args) == 0 { 323 | actions.DisplayActions() 324 | } else { 325 | actionName := args[0] 326 | actions.DisplayActionInfo(actionName) 327 | } 328 | }, 329 | } 330 | 331 | // FUNCTIONS 332 | 333 | func toggleVerbosity(cmd *cobra.Command, args []string) { 334 | shared.Verbose = verbose 335 | if verbose { 336 | slog.SetLogLoggerLevel(slog.LevelInfo) 337 | } else { 338 | slog.SetLogLoggerLevel(slog.LevelWarn) 339 | } 340 | } 341 | 342 | func setListenerOptions() { 343 | options.AutoFetchABI = AutoFetchABI 344 | options.HistoricalOptions.ContinueToRealtime = ContinueToRealtime 345 | 346 | // set fromBlock 347 | if fromBlockStr != "" { 348 | fromBlock := big.NewInt(0) 349 | fromBlock, ok := fromBlock.SetString(fromBlockStr, 10) 350 | if !ok { 351 | log.Fatalf("Error when setting fromBlock, value: %v", fromBlockStr) 352 | } 353 | options.HistoricalOptions.FromBlock = fromBlock 354 | 355 | // set toBlock 356 | if toBlockStr != "" { 357 | toBlock := big.NewInt(0) 358 | toBlock, ok = toBlock.SetString(toBlockStr, 10) 359 | if !ok { 360 | log.Fatalf("Error when setting toBlock, value: %v", toBlockStr) 361 | } 362 | options.HistoricalOptions.ToBlock = toBlock 363 | 364 | // if fromBlock > toBlock, loop backwards 365 | if fromBlock.Cmp(toBlock) == 1 { 366 | options.HistoricalOptions.LoopBackwards = true 367 | } else { 368 | options.HistoricalOptions.LoopBackwards = false 369 | } 370 | } 371 | } 372 | 373 | // set stepBlocks 374 | if stepBlocksStr == "" { 375 | // set default value 376 | options.HistoricalOptions.StepBlocks = big.NewInt(10000) 377 | } else { 378 | stepBlocks := big.NewInt(0) 379 | stepBlocks, ok := stepBlocks.SetString(stepBlocksStr, 10) 380 | if !ok { 381 | log.Fatalf("Error when setting stepBlocks, value: %v", stepBlocksStr) 382 | } 383 | options.HistoricalOptions.StepBlocks = stepBlocks 384 | } 385 | 386 | options.GetBlockByNumber = GetBlockByNumber 387 | } 388 | 389 | func loadConfigFile(filename string) { 390 | if filename == "" { 391 | log.Fatalf("No config file provided (see README.md), provide with --config ") 392 | } 393 | 394 | var configOptions map[string]interface{} 395 | // load config json 396 | shared.Infof(slog.Default(), "Reading %v for config options", filename) 397 | f, _ := os.ReadFile(filename) 398 | err := json.Unmarshal(f, &configOptions) 399 | if err != nil { 400 | log.Fatal(err) 401 | } 402 | // set options 403 | // RPC URL 404 | if v, ok := configOptions["rpcurl"]; ok { 405 | options.RpcURL = v.(string) 406 | } else { 407 | log.Fatalf("Missing RPC URL in config file %v", filename) 408 | } 409 | 410 | // load actions 411 | // init maps 412 | options.AddressProperties = make(map[common.Address]map[string]interface{}) 413 | options.Actions = make(map[string]shared.ActionOptions) 414 | if v, ok := configOptions["actions"]; ok { 415 | actions := v.(map[string]interface{}) 416 | for actionName, actionConfigInterface := range actions { 417 | actionConfig := actionConfigInterface.(map[string]interface{}) 418 | // dont use action if disabled explicitly 419 | if v, ok := actionConfig["enabled"]; ok { 420 | enabled := v.(bool) 421 | if !enabled { 422 | continue 423 | } 424 | } 425 | 426 | var actionOptions shared.ActionOptions 427 | if addressesInterface, ok := actionConfig["addresses"]; ok { 428 | addresses := addressesInterface.(map[string]interface{}) 429 | // set action options 430 | 431 | for hexAddress, addressPropertiesInterface := range addresses { 432 | addressProperties := addressPropertiesInterface.(map[string]interface{}) 433 | // if address is explicitly disabled, ignore 434 | if enabled, ok := addressProperties["enabled"]; ok { 435 | if !enabled.(bool) { 436 | continue 437 | } 438 | } 439 | 440 | address := common.HexToAddress(hexAddress) 441 | // add address to global addresses 442 | options.FilterAddresses = append(options.FilterAddresses, address) 443 | options.AddressProperties[address] = addressProperties 444 | // add address to action specific addresses 445 | actionOptions.Addresses = append(actionOptions.Addresses, address) 446 | } 447 | } 448 | // action config 449 | if actionOptionsInterface, ok := actionConfig["options"]; ok { 450 | actionCustomOptions := actionOptionsInterface.(map[string]interface{}) 451 | actionOptions.CustomOptions = actionCustomOptions 452 | } 453 | options.Actions[actionName] = actionOptions 454 | } 455 | // remove duplicate addresses 456 | options.FilterAddresses = utils.RemoveDuplicates(options.FilterAddresses) 457 | } else { 458 | log.Fatalf("No actions specified in config file") 459 | } 460 | 461 | // get event sigs to filter for 462 | // event sigs are applied for all contracts, cannot apply individually 463 | if v, ok := configOptions["event-signatures"]; ok { 464 | eventSigs := v.([]interface{}) 465 | shared.LoadFromJson("./data/event_sigs.json", &shared.EventSigs) // (see below) 466 | for _, eventSigInterface := range eventSigs { 467 | eventSig := eventSigInterface.(string) 468 | eventSigHash := crypto.Keccak256Hash([]byte(eventSig)) 469 | 470 | // if filtering by event signatures, make sure that these 471 | // signatures actually exist in the event signatures data file 472 | // otherwise they are unknown events (no ABI to decode them) 473 | _, ok := shared.EventSigs[eventSigHash.Hex()] 474 | if !ok { 475 | log.Fatalf("Event ABI for '%v', does not exist, please add its ABI to the event signatures data file", eventSig) 476 | } 477 | // event sig is topic[0] 478 | if len(options.FilterEventTopics) <= 0 { 479 | options.FilterEventTopics = append(options.FilterEventTopics, []common.Hash{}) 480 | } 481 | options.FilterEventTopics[0] = append(options.FilterEventTopics[0], eventSigHash) 482 | } 483 | } 484 | 485 | // get event topics to filter for 486 | // event sigs are technically just topic[0], so we will aggregate these together 487 | if v, ok := configOptions["event-topics"]; ok { 488 | eventTopics := v.([]interface{}) 489 | for ind, eventTopicsInt := range eventTopics { 490 | topicInterfaces := eventTopicsInt.([]interface{}) 491 | var topicHashes []common.Hash 492 | for _, topicInt := range topicInterfaces { 493 | topic := topicInt.(string) 494 | topicHash := common.HexToHash(topic) 495 | topicHashes = append(topicHashes, topicHash) 496 | } 497 | if len(options.FilterEventTopics) <= ind { 498 | options.FilterEventTopics = append(options.FilterEventTopics, []common.Hash{}) 499 | } 500 | options.FilterEventTopics[ind] = append(options.FilterEventTopics[ind], topicHashes...) 501 | } 502 | } 503 | 504 | if v, ok := configOptions["l2"]; ok { 505 | options.IsL2Chain = v.(bool) 506 | } 507 | 508 | if v, ok := configOptions["max-requests-per-second"]; ok { 509 | options.MaxRequestsPerSecond = int(v.(float64)) 510 | } 511 | 512 | options.HistoricalOptions.BatchFetchBlocks = batchFetchBlocks 513 | } 514 | 515 | func init() { 516 | 517 | // parse flags 518 | rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "Verbose output") 519 | 520 | // realtime 521 | trackCmd.PersistentFlags().BoolVar(&AutoFetchABI, "fetch-abi", false, "Automatically get contract ABI for event signatures from blockscanners") 522 | // trackCmd.PersistentFlags().BoolVar(&SingleSubscriptionLog, "single-sub", false, "Use one subscription log for all contracts. Cannot filter event signatures individually for each contract if doing so.") 523 | 524 | // specify config file with options, instead of providing through command line 525 | trackCmd.PersistentFlags().StringVar(&configFilepath, "config", "", "Path the config file instead of specifying settings through command line") 526 | trackCmd.PersistentFlags().BoolVar(&GetBlockByNumber, "get-block-by-number", false, "Whether to try getting blocks by number first, rather than by hash.") 527 | 528 | // data 529 | 530 | // add event sigs 531 | addEventsCmd.PersistentFlags().StringVar(&abiFilepath, "abi", "", "Path to the abi file") 532 | addEventsCmd.PersistentFlags().StringVar(&dataContractAddresses, "contract", "", "Contract addresses, comma separated") 533 | addEventsCmd.PersistentFlags().StringVar(&chainIdStr, "chain", "", "Chain ID contract is deployed on") 534 | addEventsCmd.MarkFlagsOneRequired("abi", "contract") 535 | addEventsCmd.MarkFlagsMutuallyExclusive("abi", "contract") 536 | addEventsCmd.MarkFlagsRequiredTogether("contract", "chain") 537 | 538 | // add func sigs 539 | addFuncSigCmd.PersistentFlags().StringVar(&abiFilepath, "abi", "", "Path to the abi file") 540 | addFuncSigCmd.PersistentFlags().StringVar(&dataContractAddresses, "contract", "", "Contract addresses, comma separated") 541 | addFuncSigCmd.PersistentFlags().StringVar(&chainIdStr, "chain", "", "Chain ID contract is deployed on") 542 | addFuncSigCmd.MarkFlagsOneRequired("abi", "contract") 543 | addFuncSigCmd.MarkFlagsMutuallyExclusive("abi", "contract") 544 | addFuncSigCmd.MarkFlagsRequiredTogether("contract", "chain") 545 | 546 | // historical 547 | // historicalCmd.PersistentFlags().BoolVar(&ContinueToRealtime, "continue-realtime", false, "Starts listening for and processing realtime events/block after historical is done. End block range must not be set.") 548 | historicalCmd.PersistentFlags().StringVar(&fromBlockStr, "from-block", "", "Block to start from when doing historical tracking") 549 | historicalCmd.PersistentFlags().StringVar(&toBlockStr, "to-block", "", "Block to stop at (inclusive) when doing historical tracking") 550 | historicalCmd.PersistentFlags().StringVar(&stepBlocksStr, "step-blocks", "", "How many blocks to request at a time, only relevant for historical event filter log.") 551 | 552 | historicalCmd.PersistentFlags().BoolVar(&batchFetchBlocks, "batch-fetch-blocks", false, "Whether or not to fetch multiple blocks asyncronously (currently in testing)") 553 | 554 | // listening to pending (unconfirmed) blocks 555 | realtimeBlocksCmd.PersistentFlags().BoolVar(&PendingBlocks, "pending-blocks", false, "Whether or not to listen for pending (unconfirmed) blocks") 556 | 557 | // root cmd structure 558 | // root 559 | // - track 560 | // - realtime 561 | // - events 562 | // - blocks 563 | // - historical 564 | // - events 565 | // - blocks 566 | // - data 567 | // - add 568 | // - actions 569 | rootCmd.AddCommand(trackCmd) 570 | rootCmd.AddCommand(dataCmd) 571 | rootCmd.AddCommand(displayActionsCmd) 572 | 573 | trackCmd.AddCommand(realtimeCmd) 574 | trackCmd.AddCommand(historicalCmd) 575 | realtimeCmd.AddCommand(realtimeEventsCmd) 576 | realtimeCmd.AddCommand(realtimeBlocksCmd) 577 | historicalCmd.AddCommand(historicalEventsCmd) 578 | historicalCmd.AddCommand(historicalBlocksCmd) 579 | 580 | dataCmd.AddCommand(addEventsCmd) 581 | dataCmd.AddCommand(addFuncSigCmd) 582 | } 583 | 584 | func executeArgs() { 585 | if err := rootCmd.Execute(); err != nil { 586 | fmt.Println(err) 587 | os.Exit(1) 588 | } 589 | } 590 | 591 | func main() { 592 | executeArgs() 593 | } 594 | -------------------------------------------------------------------------------- /shared/common.go: -------------------------------------------------------------------------------- 1 | package shared 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "log" 8 | "log/slog" 9 | "math/big" 10 | "os" 11 | "sync" 12 | 13 | "github.com/ethereum/go-ethereum/common" 14 | "github.com/ethereum/go-ethereum/ethclient" 15 | "github.com/ethereum/go-ethereum/rpc" 16 | ) 17 | 18 | // GLOBAL VARIABLES 19 | 20 | var Client *ethclient.Client 21 | var ChainID *big.Int 22 | 23 | // command args (to be set by main.go) 24 | var RpcURL string 25 | var Verbose bool 26 | var Options TrackooorOptions 27 | 28 | // JSON data 29 | var EventSigs map[string]interface{} // from data file, maps hex string to event abi 30 | var FuncSigs map[string]interface{} // from data file, maps hex string (func selector) to func abi 31 | var Blockscanners map[string]interface{} 32 | 33 | // ERC20 data 34 | var ERC20TokenInfosMutex sync.RWMutex 35 | var ERC20TokenInfos map[common.Address]ERC20Info 36 | 37 | // cached data 38 | var AddressTypeCacheMutex sync.RWMutex 39 | var AddressTypeCache map[common.Address]int 40 | 41 | // wait groups 42 | var BlockWaitGroup sync.WaitGroup 43 | 44 | // this is to auto determine whether to track blocks or events 45 | var BlockTrackingRequired bool 46 | 47 | // STRUCTS 48 | 49 | // config options for trackooors 50 | type TrackooorOptions struct { 51 | RpcURL string 52 | FilterAddresses []common.Address // global list of addresses to filter for 53 | FilterEventTopics [][]common.Hash // global list of event topics to filter for 54 | 55 | AddressProperties map[common.Address]map[string]interface{} // e.g. map address to "name":"USDC" 56 | 57 | Actions map[string]ActionOptions // map action name to its options 58 | 59 | AutoFetchABI bool 60 | MaxRequestsPerSecond int 61 | 62 | ListenerOptions ListenerOptions 63 | HistoricalOptions HistoricalOptions 64 | 65 | // whether or not to try getting block by number first, rather than by hash 66 | GetBlockByNumber bool 67 | 68 | IsL2Chain bool // will account for invalid tx types 69 | } 70 | 71 | type ActionOptions struct { 72 | Addresses []common.Address // addresses specific to each action 73 | EventSigs []common.Hash // event sigs specific to each action 74 | CustomOptions map[string]interface{} // custom options specific to each action 75 | } 76 | 77 | type ListenerOptions struct { 78 | ListenToDeployments bool 79 | } 80 | 81 | type HistoricalOptions struct { 82 | FromBlock *big.Int 83 | ToBlock *big.Int 84 | StepBlocks *big.Int 85 | ContinueToRealtime bool 86 | LoopBackwards bool 87 | 88 | BatchFetchBlocks bool 89 | } 90 | 91 | // already processed blocks, txs or events 92 | type ProcessedEntity struct { 93 | BlockNumber *big.Int 94 | TxHash common.Hash 95 | EventIndex uint 96 | } 97 | 98 | var AlreadyProcessed map[common.Hash]ProcessedEntity 99 | var DupeDetected bool 100 | var SwitchingToRealtime bool 101 | var AreActionsInitialized bool 102 | 103 | // constants 104 | var ZeroAddress = common.HexToAddress("0x0000000000000000000000000000000000000000") 105 | 106 | // info for each event from the json 107 | type EventInfo struct { 108 | Name string 109 | Sig string 110 | Abi map[string]interface{} 111 | } 112 | 113 | // info of erc20 tokens 114 | type ERC20Info struct { 115 | Name string 116 | Symbol string 117 | Decimals uint8 118 | Address common.Address 119 | } 120 | 121 | // FUNCTIONS 122 | 123 | func init() { 124 | // init maps 125 | ERC20TokenInfos = make(map[common.Address]ERC20Info) 126 | AddressTypeCache = make(map[common.Address]int) 127 | } 128 | 129 | func Infof(logger *slog.Logger, format string, args ...any) { 130 | logger.Info(fmt.Sprintf(format, args...)) 131 | } 132 | 133 | func Warnf(logger *slog.Logger, format string, args ...any) { 134 | logger.Warn(fmt.Sprintf(format, args...)) 135 | } 136 | 137 | func TimeLogger(whichController string) *log.Logger { 138 | return log.New(os.Stderr, whichController, log.Lmsgprefix|log.LstdFlags) 139 | } 140 | 141 | func ConnectToRPC(rpcURL string) (*ethclient.Client, *big.Int) { 142 | Infof(slog.Default(), "Connecting to RPC URL...\n") 143 | // client, err := ethclient.Dial(rpcURL) 144 | RpcClient, err := rpc.DialOptions( 145 | context.Background(), 146 | rpcURL, 147 | rpc.WithWebsocketMessageSizeLimit(0), // no limit 148 | ) 149 | client := ethclient.NewClient(RpcClient) 150 | 151 | if err != nil { 152 | log.Fatal(err) 153 | } 154 | Infof(slog.Default(), "Connected\n") 155 | Infof(slog.Default(), "Fetching Chain ID...\n") 156 | chainID, err := client.NetworkID(context.Background()) 157 | if err != nil { 158 | log.Fatal(err) 159 | } 160 | Infof(slog.Default(), "Chain ID: %d\n", chainID) 161 | return client, chainID 162 | } 163 | 164 | func LoadFromJson(filename string, v any) { 165 | Infof(slog.Default(), "Loading JSON file %v", filename) 166 | f, _ := os.ReadFile(filename) 167 | err := json.Unmarshal(f, v) 168 | if err != nil { 169 | log.Fatalf("LoadFromJson error: %v\n", err) 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /shared/constants.go: -------------------------------------------------------------------------------- 1 | package shared 2 | 3 | var ChainIdName = map[string]string{ 4 | "1": "ethereum", 5 | "8": "ubiq", 6 | "10": "optimism", 7 | "14": "flare", 8 | "19": "songbird", 9 | "20": "elastos", 10 | "24": "kardia", 11 | "25": "cronos", 12 | "30": "rsk", 13 | "40": "telos", 14 | "42": "lukso", 15 | "44": "crab", 16 | "46": "darwinia", 17 | "50": "xdc", 18 | "52": "csc", 19 | "55": "zyx", 20 | "56": "binance", 21 | "57": "syscoin", 22 | "60": "gochain", 23 | "61": "ethereumclassic", 24 | "66": "okexchain", 25 | "70": "hoo", 26 | "82": "meter", 27 | "87": "nova network", 28 | "88": "tomochain", 29 | "96": "bitkub", 30 | "100": "xdai", 31 | "106": "velas", 32 | "108": "thundercore", 33 | "119": "enuls", 34 | "122": "fuse", 35 | "128": "heco", 36 | "130": "unichain", 37 | "137": "polygon", 38 | "146": "sonic", 39 | "148": "shimmer_evm", 40 | "151": "rbn", 41 | "166": "omni", 42 | "169": "manta", 43 | "177": "hsk", 44 | "181": "water", 45 | "196": "xlayer", 46 | "200": "xdaiarb", 47 | "204": "op_bnb", 48 | "207": "vinuchain", 49 | "246": "energyweb", 50 | "248": "oasys", 51 | "250": "fantom", 52 | "252": "fraxtal", 53 | "269": "hpb", 54 | "288": "boba", 55 | "311": "omax", 56 | "314": "filecoin", 57 | "321": "kucoin", 58 | "324": "zksync era", 59 | "336": "shiden", 60 | "361": "theta", 61 | "369": "pulse", 62 | "388": "cronos zkevm", 63 | "416": "sx", 64 | "463": "areon", 65 | "478": "form network", 66 | "480": "wc", 67 | "534": "candle", 68 | "570": "rollux", 69 | "592": "astar", 70 | "690": "redstone", 71 | "698": "matchain", 72 | "820": "callisto", 73 | "841": "tara", 74 | "888": "wanchain", 75 | "957": "lyra chain", 76 | "996": "bifrost", 77 | "1030": "conflux", 78 | "1088": "metis", 79 | "1100": "dymension", 80 | "1101": "polygon zkevm", 81 | "1116": "core", 82 | "1135": "lisk", 83 | "1231": "ultron", 84 | "1234": "step", 85 | "1284": "moonbeam", 86 | "1285": "moonriver", 87 | "1329": "sei", 88 | "1440": "living assets mainnet", 89 | "1514": "sty", 90 | "1559": "tenet", 91 | "1625": "gravity", 92 | "1729": "reya network", 93 | "1868": "soneium", 94 | "1923": "swellchain", 95 | "1975": "onus", 96 | "1992": "hubblenet", 97 | "1996": "sanko", 98 | "2000": "dogechain", 99 | "2001": "milkomeda", 100 | "2002": "milkomeda_a1", 101 | "2222": "kava", 102 | "2332": "soma", 103 | "2410": "karak", 104 | "2741": "abstract", 105 | "2818": "morph", 106 | "4158": "crossfi", 107 | "4337": "beam", 108 | "4689": "iotex", 109 | "5000": "mantle", 110 | "5050": "xlc", 111 | "5551": "nahmii", 112 | "6001": "bouncebit", 113 | "6969": "tombchain", 114 | "7000": "zetachain", 115 | "7070": "planq", 116 | "7171": "bitrock", 117 | "7200": "xsat", 118 | "7560": "cyeth", 119 | "7700": "canto", 120 | "8217": "klaytn", 121 | "8428": "that", 122 | "8453": "base", 123 | "8668": "hela", 124 | "8822": "iotaevm", 125 | "8899": "jbc", 126 | "9001": "evmos", 127 | "9790": "carbon", 128 | "10000": "smartbch", 129 | "11820": "artela", 130 | "13371": "immutable zkevm", 131 | "15551": "loop", 132 | "16507": "genesys", 133 | "17777": "eos evm", 134 | "22776": "map protocol", 135 | "23294": "sapphire", 136 | "32520": "bitgert", 137 | "32659": "fusion", 138 | "32769": "zilliqa", 139 | "33139": "apechain", 140 | "41923": "edu chain", 141 | "42161": "arbitrum", 142 | "42170": "arbitrum nova", 143 | "42220": "celo", 144 | "42262": "oasis", 145 | "42420": "assetchain", 146 | "42793": "etherlink", 147 | "43111": "hemi", 148 | "43114": "avalanche", 149 | "47805": "rei", 150 | "48900": "zircuit", 151 | "50104": "sophon", 152 | "52014": "etn", 153 | "55244": "superposition", 154 | "55555": "reichain", 155 | "56288": "boba_bnb", 156 | "57073": "ink", 157 | "59144": "linea", 158 | "60808": "bob", 159 | "71402": "godwoken", 160 | "80094": "berachain", 161 | "81457": "blast", 162 | "88888": "chiliz", 163 | "105105": "stratis", 164 | "111188": "real", 165 | "153153": "odyssey", 166 | "167000": "taiko", 167 | "200901": "bitlayer", 168 | "222222": "hydradx", 169 | "322202": "parex", 170 | "333999": "polis", 171 | "420420": "kekchain", 172 | "534352": "scroll", 173 | "543210": "zero_network", 174 | "810180": "zklink nova", 175 | "888888": "vision", 176 | "7225878": "saakuru", 177 | "7777777": "zora", 178 | "21000000": "corn", 179 | "245022934": "neon", 180 | "994873017": "lumia", 181 | "1313161554": "aurora", 182 | "1666600000": "harmony", 183 | "11297108109": "palm", 184 | "383414847825": "zeniq", 185 | "836542336838601": "curio", 186 | } 187 | -------------------------------------------------------------------------------- /shared/decoding.go: -------------------------------------------------------------------------------- 1 | package shared 2 | 3 | import ( 4 | "encoding/json" 5 | "strings" 6 | 7 | "github.com/ethereum/go-ethereum/accounts/abi" 8 | "github.com/ethereum/go-ethereum/common" 9 | ) 10 | 11 | type EventField struct { 12 | FieldName string 13 | FieldType string 14 | Indexed bool 15 | } 16 | 17 | type EventFields []EventField 18 | 19 | func DecodeTopicsAndData(topics []common.Hash, data []byte, _abi interface{}) (EventFields, map[string]interface{}, map[string]interface{}) { 20 | // get event abi 21 | abiMap := _abi.(map[string]interface{}) 22 | eventName := abiMap["name"].(string) 23 | jsonBytes, _ := json.Marshal(_abi) 24 | jsonStr := string(jsonBytes) 25 | eventAbi, _ := abi.JSON(strings.NewReader("[" + string(jsonStr) + "]")) 26 | eventInfo := eventAbi.Events[eventName] 27 | 28 | // get event field names & types (in order) 29 | var eventFields EventFields 30 | inputs := abiMap["inputs"].([]interface{}) 31 | for _, _input := range inputs { 32 | input := _input.(map[string]interface{}) 33 | eventFields = append(eventFields, EventField{ 34 | FieldName: input["name"].(string), 35 | FieldType: input["type"].(string), 36 | Indexed: input["indexed"].(bool), 37 | }) 38 | } 39 | 40 | // decode topics 41 | indexed := make([]abi.Argument, 0) 42 | for _, input := range eventInfo.Inputs { 43 | if input.Indexed { 44 | indexed = append(indexed, input) 45 | } 46 | } 47 | decodedTopics := make(map[string]interface{}) 48 | abi.ParseTopicsIntoMap(decodedTopics, indexed, topics) 49 | 50 | // decode data 51 | decodedData := make(map[string]interface{}) 52 | eventInfo.Inputs.UnpackIntoMap(decodedData, data) 53 | 54 | // fmt.Printf("decodedTopics: %v\n", decodedTopics) 55 | // fmt.Printf("decodedData: %v\n", decodedData) 56 | return eventFields, decodedTopics, decodedData 57 | } 58 | 59 | // returns function signature, and map function parameter name to value (argument) 60 | func DecodeTransaction(funcSelectorHex string, txData []byte, _abi interface{}) (string, map[string]interface{}, error) { 61 | abiMap := _abi.(map[string]interface{}) 62 | funcSig := abiMap["sig"].(string) 63 | jsonBytes, _ := json.Marshal(abiMap["abi"]) 64 | jsonStr := string(jsonBytes) 65 | funcAbi, _ := abi.JSON(strings.NewReader("[" + string(jsonStr) + "]")) 66 | 67 | method, err := funcAbi.MethodById(common.Hex2Bytes(funcSelectorHex[2:])) 68 | if err != nil { 69 | return funcSig, nil, err 70 | } 71 | 72 | var args = make(map[string]interface{}) 73 | method.Inputs.UnpackIntoMap(args, txData[4:]) 74 | 75 | return funcSig, args, nil 76 | } 77 | -------------------------------------------------------------------------------- /shared/helpers.go: -------------------------------------------------------------------------------- 1 | package shared 2 | 3 | import ( 4 | "context" 5 | "encoding/hex" 6 | "fmt" 7 | "log" 8 | "log/slog" 9 | "math/big" 10 | "reflect" 11 | "sync" 12 | 13 | "github.com/Zellic/EVM-trackooor/contracts/IERC20Metadata" 14 | 15 | "github.com/ethereum/go-ethereum/accounts/abi/bind" 16 | "github.com/ethereum/go-ethereum/common" 17 | "github.com/ethereum/go-ethereum/core/types" 18 | ) 19 | 20 | const ( 21 | DeploymentTx = iota 22 | RegularTx 23 | ContractTx 24 | ) 25 | 26 | var TxTypes = map[int]string{ 27 | DeploymentTx: "Deployment", 28 | RegularTx: "EOA", 29 | ContractTx: "Contract", 30 | } 31 | 32 | // cache blocks as to not query same block number multiple times 33 | // warning: this may use a lot of memory 34 | var CachedBlocks map[*big.Int]*types.Header 35 | var CachedBlocksMutex = sync.RWMutex{} // prevent race condition for map CachedBlocks 36 | 37 | func GetBlockHeader(blockNum *big.Int) *types.Header { 38 | var block *types.Header 39 | var err error 40 | 41 | CachedBlocksMutex.RLock() 42 | if v, ok := CachedBlocks[blockNum]; ok { 43 | CachedBlocksMutex.RUnlock() 44 | 45 | block = v 46 | } else { 47 | CachedBlocksMutex.RUnlock() 48 | 49 | block, err = Client.HeaderByNumber(context.Background(), blockNum) 50 | if err != nil { 51 | log.Fatalf(`Failed to retrieve info for block %v, error "%v"\n`, blockNum, err) 52 | } 53 | CachedBlocksMutex.Lock() 54 | CachedBlocks[blockNum] = block 55 | CachedBlocksMutex.Unlock() 56 | } 57 | return block 58 | } 59 | 60 | // determine action type of transaction. 61 | // requires 1 RPC request unless cache used. 62 | // not to be confused with numerical types such as for legacy tx, access list tx etc. 63 | // three types possible: regular (EOA to EOA), deployment, execution (of smart contracts) 64 | func DetermineTxType(tx *types.Transaction, blockNumber *big.Int) int { 65 | if tx.To() == nil { // contract deployment 66 | return DeploymentTx 67 | } 68 | to := *tx.To() 69 | return DetermineAddressType(to, blockNumber) 70 | } 71 | 72 | // returns whether an address is a contract or EOA, at a block number, costing 1 query if address uncached. 73 | // result is cached, which however will not take into the block number. assumes contracts stay as contracts 74 | func DetermineAddressType(address common.Address, blockNumber *big.Int) int { 75 | // check if we already know address type in cache 76 | // although cache does not take into account block number, we will assume 77 | // addresses that were contracts will stay as contracts 78 | AddressTypeCacheMutex.RLock() 79 | if v, ok := AddressTypeCache[address]; ok { 80 | AddressTypeCacheMutex.RUnlock() 81 | return v 82 | } 83 | AddressTypeCacheMutex.RUnlock() 84 | 85 | // check if 'to' has bytecode. if not, then it is an EOA 86 | // caches result in `AddressTypeCache` 87 | bytecode, err := Client.CodeAt(context.Background(), address, blockNumber) 88 | if err != nil { 89 | log.Fatal(err) 90 | } 91 | if len(bytecode) == 0 { 92 | AddressTypeCacheMutex.Lock() 93 | AddressTypeCache[address] = RegularTx 94 | AddressTypeCacheMutex.Unlock() 95 | return RegularTx 96 | } 97 | AddressTypeCacheMutex.Lock() 98 | AddressTypeCache[address] = ContractTx 99 | AddressTypeCacheMutex.Unlock() 100 | return ContractTx 101 | } 102 | 103 | // returns deployed contract address given a tx 104 | func GetDeployedContractAddress(tx *types.Transaction) (common.Address, error) { 105 | txReceipt, err := Client.TransactionReceipt(context.Background(), tx.Hash()) 106 | if err != nil { 107 | // log.Fatalf("Failed to get transaction receipt of tx %v: %v", tx.Hash(), err) 108 | Warnf(slog.Default(), "Failed to get transaction receipt of tx %v: %v", tx.Hash(), err) 109 | } 110 | return txReceipt.ContractAddress, err 111 | } 112 | 113 | func BytesToHex(data interface{}) (string, error) { 114 | v := reflect.ValueOf(data) 115 | 116 | // Ensure the value is of slice or array kind 117 | if v.Kind() != reflect.Slice && v.Kind() != reflect.Array { 118 | return "", fmt.Errorf("unsupported type: %s, expected slice or array", v.Kind().String()) 119 | } 120 | 121 | // Ensure the elements are of the type uint8 (aka byte) 122 | if v.Type().Elem().Kind() != reflect.Uint8 { 123 | return "", fmt.Errorf("unsupported element type: %s, expected uint8 (byte)", v.Type().Elem().Kind().String()) 124 | } 125 | 126 | // Create a byte slice from the reflected value 127 | bytes := make([]byte, v.Len()) 128 | for i := 0; i < v.Len(); i++ { 129 | bytes[i] = byte(v.Index(i).Uint()) 130 | } 131 | 132 | // Convert the byte slice to a hexadecimal string 133 | return hex.EncodeToString(bytes), nil 134 | } 135 | 136 | func RetrieveERC20Info(tokenAddress common.Address) ERC20Info { 137 | var erc20Info ERC20Info 138 | 139 | // check if we already fetch the info. if so, returned the cached info 140 | ERC20TokenInfosMutex.RLock() 141 | if v, ok := ERC20TokenInfos[tokenAddress]; ok { 142 | ERC20TokenInfosMutex.RUnlock() 143 | return v 144 | } 145 | ERC20TokenInfosMutex.RUnlock() 146 | 147 | tokenInstance, err := IERC20Metadata.NewIERC20Metadata(tokenAddress, Client) 148 | if err != nil { 149 | log.Fatal(err) 150 | } 151 | 152 | Infof(slog.Default(), "Retrieving ERC20 token info...\n") 153 | tokenName, err := tokenInstance.Name(&bind.CallOpts{}) 154 | if err != nil { 155 | Warnf(slog.Default(), "Could not retrieve token name of %v!\n", tokenAddress) 156 | } 157 | 158 | tokenDecimals, err := tokenInstance.Decimals(&bind.CallOpts{}) 159 | if err != nil { 160 | Warnf(slog.Default(), "Could not retrieve token decimals of %v!\n", tokenAddress) 161 | } 162 | 163 | tokenSymbol, err := tokenInstance.Symbol(&bind.CallOpts{}) 164 | if err != nil { 165 | Warnf(slog.Default(), "Could not retrieve token symbol of %v!\n", tokenAddress) 166 | } 167 | 168 | Infof(slog.Default(), "Retrieved token name '%v'\n", tokenName) 169 | Infof(slog.Default(), "Retrieved token decimals '%v'\n", tokenDecimals) 170 | Infof(slog.Default(), "Retrieved token symbol '%v'\n", tokenSymbol) 171 | 172 | erc20Info.Name = tokenName 173 | erc20Info.Symbol = tokenSymbol 174 | erc20Info.Decimals = tokenDecimals 175 | erc20Info.Address = tokenAddress 176 | 177 | // cache to ERC20TokenInfos mapping so we don't retrieve again 178 | ERC20TokenInfosMutex.Lock() 179 | ERC20TokenInfos[tokenAddress] = erc20Info 180 | ERC20TokenInfosMutex.Unlock() 181 | 182 | return erc20Info 183 | } 184 | -------------------------------------------------------------------------------- /shared/l2helpers.go: -------------------------------------------------------------------------------- 1 | package shared 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "math/big" 8 | "sync/atomic" 9 | "time" 10 | 11 | "github.com/ethereum/go-ethereum/common" 12 | "github.com/ethereum/go-ethereum/core/types" 13 | ) 14 | 15 | // hacky code for L2 blocks 16 | 17 | type rpcBlock struct { 18 | Hash common.Hash `json:"hash"` 19 | Transactions []rpcTransaction `json:"transactions"` 20 | UncleHashes []common.Hash `json:"uncles"` 21 | Withdrawals []*types.Withdrawal `json:"withdrawals,omitempty"` 22 | } 23 | 24 | type rpcTransaction struct { 25 | tx *types.Transaction 26 | // tx *Transaction 27 | txExtraInfo 28 | } 29 | 30 | // Transaction types. 31 | const ( 32 | LegacyTxType = 0x00 33 | AccessListTxType = 0x01 34 | DynamicFeeTxType = 0x02 35 | BlobTxType = 0x03 36 | ) 37 | 38 | // Transaction is an Ethereum transaction. 39 | type Transaction struct { 40 | inner TxData // Consensus contents of a transaction 41 | time time.Time // Time first seen locally (spam avoidance) 42 | 43 | // caches 44 | hash atomic.Pointer[common.Hash] 45 | size atomic.Uint64 46 | from atomic.Pointer[sigCache] 47 | } 48 | 49 | // sigCache is used to cache the derived sender and contains 50 | // the signer used to derive it. 51 | type sigCache struct { 52 | signer Signer 53 | from common.Address 54 | } 55 | 56 | // Signer encapsulates transaction signature handling. The name of this type is slightly 57 | // misleading because Signers don't actually sign, they're just for validating and 58 | // processing of signatures. 59 | // 60 | // Note that this interface is not a stable API and may change at any time to accommodate 61 | // new protocol rules. 62 | type Signer interface { 63 | // Sender returns the sender address of the transaction. 64 | Sender(tx *Transaction) (common.Address, error) 65 | 66 | // SignatureValues returns the raw R, S, V values corresponding to the 67 | // given signature. 68 | SignatureValues(tx *Transaction, sig []byte) (r, s, v *big.Int, err error) 69 | ChainID() *big.Int 70 | 71 | // Hash returns 'signature hash', i.e. the transaction hash that is signed by the 72 | // private key. This hash does not uniquely identify the transaction. 73 | Hash(tx *Transaction) common.Hash 74 | 75 | // Equal returns true if the given signer is the same as the receiver. 76 | Equal(Signer) bool 77 | } 78 | 79 | // AccessList is an EIP-2930 access list. 80 | type AccessList []AccessTuple 81 | 82 | // AccessTuple is the element type of an access list. 83 | type AccessTuple struct { 84 | Address common.Address `json:"address" gencodec:"required"` 85 | StorageKeys []common.Hash `json:"storageKeys" gencodec:"required"` 86 | } 87 | 88 | type TxData interface { 89 | txType() byte // returns the type ID 90 | copy() TxData // creates a deep copy and initializes all fields 91 | 92 | chainID() *big.Int 93 | accessList() AccessList 94 | data() []byte 95 | gas() uint64 96 | gasPrice() *big.Int 97 | gasTipCap() *big.Int 98 | gasFeeCap() *big.Int 99 | value() *big.Int 100 | nonce() uint64 101 | to() *common.Address 102 | 103 | rawSignatureValues() (v, r, s *big.Int) 104 | setSignatureValues(chainID, v, r, s *big.Int) 105 | 106 | // effectiveGasPrice computes the gas price paid by the transaction, given 107 | // the inclusion block baseFee. 108 | // 109 | // Unlike other TxData methods, the returned *big.Int should be an independent 110 | // copy of the computed value, i.e. callers are allowed to mutate the result. 111 | // Method implementations can use 'dst' to store the result. 112 | effectiveGasPrice(dst *big.Int, baseFee *big.Int) *big.Int 113 | 114 | encode(*bytes.Buffer) error 115 | decode([]byte) error 116 | } 117 | 118 | func (tx *rpcTransaction) UnmarshalJSON(msg []byte) error { 119 | if err := json.Unmarshal(msg, &tx.tx); err != nil { 120 | return err 121 | } 122 | return json.Unmarshal(msg, &tx.txExtraInfo) 123 | } 124 | 125 | type txExtraInfo struct { 126 | BlockNumber *string `json:"blockNumber,omitempty"` 127 | BlockHash *common.Hash `json:"blockHash,omitempty"` 128 | From *common.Address `json:"from,omitempty"` 129 | } 130 | 131 | // get a block by number hex string, or `latest` / `pending` / `earliest` 132 | func GetL2BlockByHexNumber(number string) (*types.Block, error) { 133 | var raw json.RawMessage 134 | err := Client.Client().CallContext( 135 | context.Background(), 136 | &raw, 137 | "eth_getBlockByNumber", 138 | number, 139 | true, 140 | ) 141 | if err != nil { 142 | return nil, err 143 | } 144 | 145 | // fmt.Printf("raw: %v\n", string(raw)) 146 | 147 | // Decode header and transactions. 148 | var head *types.Header 149 | if err := json.Unmarshal(raw, &head); err != nil { 150 | panic(err) 151 | // return nil, err 152 | } 153 | // fmt.Printf("head: %v\n", head) 154 | var body rpcBlock 155 | if err := json.Unmarshal(raw, &body); err != nil { 156 | panic(err) 157 | // return nil, err 158 | } 159 | // fmt.Printf("body: %v\n", body) 160 | 161 | txs := make([]*types.Transaction, len(body.Transactions)) 162 | for i, tx := range body.Transactions { 163 | txs[i] = tx.tx 164 | // properTx := types.Transaction{} 165 | // err := copier.Copy(&properTx, tx.tx) 166 | // txs[i] = &properTx 167 | // if err != nil { 168 | // log.Fatalf("err: %v\n", err) 169 | // } 170 | // // fmt.Printf("tx.tx: %v\n", tx.tx) 171 | // // fmt.Printf("txs[i]: %v\n", txs[i]) 172 | } 173 | 174 | return types.NewBlockWithHeader(head).WithBody( 175 | types.Body{ 176 | Transactions: txs, 177 | // Uncles: uncles, 178 | Withdrawals: body.Withdrawals, 179 | }), nil 180 | } 181 | -------------------------------------------------------------------------------- /shared/tracing.go: -------------------------------------------------------------------------------- 1 | package shared 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "log" 8 | "math/big" 9 | 10 | "github.com/ethereum/go-ethereum/common" 11 | "github.com/ethereum/go-ethereum/common/hexutil" 12 | ) 13 | 14 | // debug_traceTransaction code 15 | 16 | type TraceResult struct { 17 | From common.Address `json:"from"` 18 | To common.Address `json:"to"` 19 | Gas uint64 `json:"gas"` 20 | GasUsed uint64 `json:"gasUsed"` 21 | Input []byte `json:"input"` 22 | Output []byte `json:"output"` 23 | Value *big.Int `json:"value"` 24 | Type string `json:"type"` 25 | Calls []TraceResult `json:"calls"` 26 | Error string `json:"error"` 27 | } 28 | 29 | func (t *TraceResult) UnmarshalJSON(input []byte) error { 30 | type traceResultTmp struct { 31 | From common.Address `json:"from"` 32 | To common.Address `json:"to"` 33 | Gas hexutil.Uint64 `json:"gas"` 34 | GasUsed hexutil.Uint64 `json:"gasUsed"` 35 | Input hexutil.Bytes `json:"input"` 36 | Output hexutil.Bytes `json:"output"` 37 | Value *hexutil.Big `json:"value"` 38 | Type string `json:"type"` 39 | Calls []TraceResult `json:"calls"` 40 | } 41 | 42 | var dec traceResultTmp 43 | if err := json.Unmarshal(input, &dec); err != nil { 44 | return err 45 | } 46 | 47 | t.From = dec.From 48 | t.To = dec.To 49 | t.Gas = uint64(dec.Gas) 50 | t.GasUsed = uint64(dec.GasUsed) 51 | t.Input = dec.Input 52 | t.Output = dec.Output 53 | t.Value = (*big.Int)(dec.Value) 54 | t.Type = dec.Type 55 | t.Calls = dec.Calls 56 | 57 | return nil 58 | } 59 | 60 | func Debug_traceTransaction(txHash common.Hash) (TraceResult, error) { 61 | tracer := make(map[string]interface{}) 62 | tracer["tracer"] = "callTracer" 63 | 64 | var raw json.RawMessage 65 | Client.Client().CallContext( 66 | context.Background(), 67 | &raw, 68 | "debug_traceTransaction", 69 | txHash.Hex(), 70 | tracer, 71 | ) 72 | 73 | var tResult TraceResult 74 | if err := json.Unmarshal(raw, &tResult); err != nil { 75 | // fmt.Printf("Debug_traceTransaction err txHash: %v err: %v\n", txHash, err) 76 | return TraceResult{}, fmt.Errorf("err: %v raw: %v\n", err, raw) 77 | // return Debug_traceTransaction(txHash) 78 | } 79 | return tResult, nil 80 | } 81 | 82 | // trace_transaction and trace_replayBlockTransactions 83 | 84 | type Action struct { 85 | From common.Address `json:"from"` 86 | To common.Address `json:"to,omitempty"` 87 | CallType string `json:"callType,omitempty"` 88 | Gas hexutil.Uint64 `json:"gas"` 89 | Input hexutil.Bytes `json:"input,omitempty"` 90 | Init hexutil.Bytes `json:"init,omitempty"` 91 | Value *hexutil.Big `json:"value"` 92 | 93 | Address common.Address `json:"address,omitempty"` 94 | RefundAddress common.Address `json:"refundAddress,omitempty"` 95 | Balance *hexutil.Big `json:"balance,omitempty"` 96 | } 97 | 98 | type Result struct { 99 | GasUsed *hexutil.Big `json:"gasUsed"` 100 | Output hexutil.Bytes `json:"output,omitempty"` 101 | Address common.Address `json:"address,omitempty"` 102 | Code hexutil.Bytes `json:"code,omitempty"` 103 | } 104 | 105 | type Trace struct { 106 | Action Action `json:"action"` 107 | BlockHash hexutil.Bytes `json:"blockHash"` 108 | BlockNumber int `json:"blockNumber"` 109 | Result Result `json:"result"` 110 | Subtraces int `json:"subtraces"` 111 | TraceAddress []int `json:"traceAddress"` 112 | TransactionPosition int `json:"transactionPosition"` 113 | Type string `json:"type"` 114 | } 115 | 116 | func Trace_transaction(txHash common.Hash) []Trace { 117 | var raw json.RawMessage 118 | Client.Client().CallContext( 119 | context.Background(), 120 | &raw, 121 | "trace_transaction", 122 | txHash.Hex(), 123 | ) 124 | 125 | // fmt.Printf("raw: %v\n", string(raw)) 126 | 127 | var result []Trace 128 | if err := json.Unmarshal([]byte(raw), &result); err != nil { 129 | fmt.Printf("Trace_transaction err txHash: %v err: %v\n", txHash, err) 130 | return nil 131 | // return Trace_transaction(txHash) 132 | } 133 | 134 | return result 135 | } 136 | 137 | // for Trace_replayBlockTransactions 138 | type BlockTrace_Replay struct { 139 | Output hexutil.Bytes `json:"output"` 140 | StateDiff any `json:"stateDiff"` 141 | Traces []Trace `json:"trace"` 142 | TransactionHash hexutil.Bytes `json:"transactionHash"` 143 | } 144 | 145 | // for Trace_block 146 | type BlockTrace struct { 147 | Action Action `json:"action"` 148 | BlockHash string `json:"blockHash"` 149 | BlockNumber *big.Int `json:"blockNumber"` 150 | Error string `json:"error"` // empty if no error 151 | Result Result `json:"result"` 152 | TraceAddress []int `json:"traceAddress"` 153 | TransactionHash string `json:"transactionHash"` 154 | TransactionPosition *big.Int `json:"transactionPosition"` 155 | Type string `json:"type"` 156 | } 157 | 158 | // traceType is one or more of "trace", "stateDiff" 159 | func Trace_replayBlockTransactions(blockNum uint64, traceType []string) ([]BlockTrace_Replay, error) { 160 | 161 | var raw json.RawMessage 162 | Client.Client().CallContext( 163 | context.Background(), 164 | &raw, 165 | "trace_replayBlockTransactions", 166 | blockNum, traceType, // params 167 | ) 168 | 169 | var result []BlockTrace_Replay 170 | if err := json.Unmarshal([]byte(raw), &result); err != nil { 171 | // fmt.Printf("Trace_replayBlockTransactions err blockNum: %v err: %v raw: %v\n", blockNum, err, raw) 172 | // return nil 173 | return nil, fmt.Errorf("Trace_replayBlockTransactions err blockNum: %v err: %v raw: %v", blockNum, err, raw) 174 | } 175 | 176 | return result, nil 177 | } 178 | 179 | func Trace_block(blockNum uint64) ([]BlockTrace, error) { 180 | 181 | var raw json.RawMessage 182 | Client.Client().CallContext( 183 | context.Background(), 184 | &raw, 185 | "trace_block", 186 | blockNum, 187 | ) 188 | 189 | var result []BlockTrace 190 | if err := json.Unmarshal([]byte(raw), &result); err != nil { 191 | // return nil, fmt.Errorf("Trace_block err blockNum: %v err: %v raw: %v", blockNum, err, raw) 192 | return nil, fmt.Errorf("Trace_block err blockNum: %v err: %v", blockNum, err) 193 | } 194 | 195 | return result, nil 196 | } 197 | 198 | type CallParams struct { 199 | From common.Address `json:"from,omitempty"` 200 | To common.Address `json:"to,omitempty"` 201 | Gas uint64 `json:"gas,omitempty"` 202 | GasPrice uint64 `json:"gasPrice,omitempty"` 203 | Value *hexutil.Big `json:"value,omitempty"` 204 | Data hexutil.Bytes `json:"data,omitempty"` 205 | } 206 | 207 | type TraceCallResult struct { 208 | Output string `json:"output"` 209 | StateDiff map[common.Address]StateEntry `json:"stateDiff"` 210 | Trace []TraceEntry `json:"trace"` 211 | VmTrace interface{} `json:"vmTrace"` // Use interface{} for null or future extensibility 212 | } 213 | 214 | type StateEntry struct { 215 | // these can either be "=" indicating nothing changed 216 | // or a json struct indicating whats changed 217 | Balance json.RawMessage `json:"balance"` 218 | Code json.RawMessage `json:"code"` 219 | Nonce json.RawMessage `json:"nonce,omitempty"` 220 | Storage json.RawMessage `json:"storage,omitempty"` 221 | } 222 | 223 | type TraceEntry struct { 224 | Action TraceAction `json:"action"` 225 | Result *CallTraceResult `json:"result,omitempty"` 226 | Subtraces int `json:"subtraces"` 227 | TraceAddress []int `json:"traceAddress"` 228 | Type string `json:"type"` 229 | Error string `json:"error,omitempty"` 230 | } 231 | 232 | type TraceAction struct { 233 | From string `json:"from"` 234 | CallType string `json:"callType"` 235 | Gas hexutil.Uint64 `json:"gas"` 236 | Input string `json:"input"` 237 | To string `json:"to"` 238 | Value *hexutil.Big `json:"value"` 239 | } 240 | 241 | type CallTraceResult struct { 242 | GasUsed hexutil.Uint64 `json:"gasUsed"` 243 | Output string `json:"output"` 244 | } 245 | 246 | // referencing https://docs.alchemy.com/reference/trace-call 247 | // traceType - Type of trace, one or more of: "trace", "stateDiff" 248 | func Trace_call(callParams CallParams, traceType []string) (TraceCallResult, error) { 249 | var raw json.RawMessage 250 | 251 | params := make(map[string]interface{}) 252 | 253 | jsonData, err := json.Marshal(callParams) 254 | if err != nil { 255 | log.Fatalf("Error marshaling to JSON: %v", err) 256 | } 257 | 258 | err = json.Unmarshal(jsonData, ¶ms) 259 | if err != nil { 260 | log.Fatalf("Error unmarshaling JSON to map: %v", err) 261 | } 262 | 263 | // fmt.Printf("params: %v\n", params) 264 | 265 | err = Client.Client().CallContext( 266 | context.Background(), 267 | &raw, 268 | "trace_call", 269 | params, // params 270 | traceType, 271 | ) 272 | if err != nil { 273 | fmt.Printf("CallContext err: %v\n", err) 274 | } 275 | 276 | // fmt.Printf("raw: %v\n", string(raw)) 277 | 278 | var result TraceCallResult 279 | if err := json.Unmarshal([]byte(raw), &result); err != nil { 280 | // fmt.Printf("Trace_replayBlockTransactions err blockNum: %v err: %v raw: %v\n", blockNum, err, raw) 281 | // return nil 282 | // return TraceCallResult{}, fmt.Errorf("Trace_call err: %v params: %v traceType: %v", err, callParams, traceType) 283 | return TraceCallResult{}, fmt.Errorf("Trace_call err: %v traceType: %v", err, traceType) 284 | } 285 | 286 | return result, nil 287 | } 288 | -------------------------------------------------------------------------------- /shared/webhook.go: -------------------------------------------------------------------------------- 1 | package shared 2 | 3 | import ( 4 | "encoding/json" 5 | "log" 6 | "log/slog" 7 | "strings" 8 | "time" 9 | 10 | discordwebhook "github.com/Zellic/EVM-trackooor/discord-webhook" 11 | ) 12 | 13 | // global webhook instance 14 | var DiscordWebhook WebhookInstance 15 | 16 | // functions will NOT error if webhook URL is not set 17 | 18 | type WebhookInstance struct { 19 | WebhookURL string 20 | Username string 21 | Avatar_url string 22 | RetrySendingMessages bool 23 | } 24 | 25 | func (w WebhookInstance) SendMessage(msg string) error { 26 | if w.WebhookURL == "" { 27 | return nil 28 | } 29 | 30 | // if message is too long, truncate it 31 | if len(msg) > 2000 { 32 | Infof(slog.Default(), "Discord message length (%v) too long, truncating", len(msg)) 33 | msg = msg[:1980] 34 | msg += "... (truncated)" 35 | } 36 | 37 | hook := discordwebhook.Hook{ 38 | Username: w.Username, 39 | Avatar_url: w.Avatar_url, 40 | Content: msg, 41 | } 42 | 43 | payload, err := json.Marshal(hook) 44 | if err != nil { 45 | log.Fatal(err) 46 | } 47 | err = discordwebhook.ExecuteWebhook(w.WebhookURL, payload) 48 | if err != nil { 49 | if strings.Contains(err.Error(), "rate limited") && w.RetrySendingMessages { 50 | Infof(slog.Default(), "Rate limited, waiting 5s and retrying sending message: %v", msg) 51 | time.Sleep(5 * time.Second) 52 | return w.SendMessage(msg) 53 | } 54 | } 55 | return err 56 | } 57 | 58 | func (w WebhookInstance) SendEmbedMessages(embeds []discordwebhook.Embed) error { 59 | if w.WebhookURL == "" { 60 | return nil 61 | } 62 | 63 | hook := discordwebhook.Hook{ 64 | Username: w.Username, 65 | Avatar_url: w.Avatar_url, 66 | Embeds: embeds, 67 | } 68 | 69 | payload, err := json.Marshal(hook) 70 | if err != nil { 71 | log.Fatal(err) 72 | } 73 | 74 | err = discordwebhook.ExecuteWebhook(w.WebhookURL, payload) 75 | if err != nil { 76 | if strings.Contains(err.Error(), "rate limited") && w.RetrySendingMessages { 77 | Infof(slog.Default(), "Rate limited, waiting 5s and retrying sending embeds (length %v)", len(embeds)) 78 | time.Sleep(5 * time.Second) 79 | return w.SendEmbedMessages(embeds) 80 | } 81 | } 82 | return err 83 | } 84 | 85 | func (w WebhookInstance) SendFile(imgData []byte, imgFilename string) error { 86 | if w.WebhookURL == "" { 87 | return nil 88 | } 89 | 90 | hook := discordwebhook.Hook{ 91 | Username: w.Username, 92 | Avatar_url: w.Avatar_url, 93 | Content: "", 94 | } 95 | 96 | payload, err := json.Marshal(hook) 97 | if err != nil { 98 | log.Fatal(err) 99 | } 100 | 101 | err = discordwebhook.SendFileToWebhook(w.WebhookURL, payload, imgData, imgFilename) 102 | if err != nil { 103 | if strings.Contains(err.Error(), "rate limited") && w.RetrySendingMessages { 104 | Infof(slog.Default(), "Rate limited, waiting 5s and retrying sending image") 105 | time.Sleep(5 * time.Second) 106 | return w.SendFile(imgData, imgFilename) 107 | } 108 | } 109 | return err 110 | } 111 | -------------------------------------------------------------------------------- /trackooor/base.go: -------------------------------------------------------------------------------- 1 | package trackooor 2 | 3 | import ( 4 | "fmt" 5 | "log/slog" 6 | 7 | "github.com/Zellic/EVM-trackooor/database" 8 | "github.com/Zellic/EVM-trackooor/shared" 9 | 10 | "github.com/ethereum/go-ethereum" 11 | "github.com/ethereum/go-ethereum/common" 12 | "github.com/ethereum/go-ethereum/core/types" 13 | ) 14 | 15 | // STRUCTS 16 | 17 | type SubscriptionLogs struct { 18 | subscription ethereum.Subscription 19 | logs chan types.Log 20 | } 21 | 22 | // FUNCTIONS 23 | 24 | func init() { 25 | shared.AlreadyProcessed = make(map[common.Hash]shared.ProcessedEntity) 26 | } 27 | 28 | func SetupListener(options shared.TrackooorOptions) { 29 | shared.Options = options 30 | 31 | fmt.Printf("Loading event signatures and blockscanners\n") 32 | shared.LoadFromJson("./data/event_sigs.json", &shared.EventSigs) 33 | shared.LoadFromJson("./data/blockscanners.json", &shared.Blockscanners) 34 | 35 | fmt.Printf("Loading function signatures\n") 36 | shared.LoadFromJson("./data/func_sigs.json", &shared.FuncSigs) 37 | 38 | shared.Client, shared.ChainID = shared.ConnectToRPC(options.RpcURL) 39 | 40 | if shared.Options.AutoFetchABI { 41 | addContractsEventAbi(shared.Options.FilterAddresses) 42 | } 43 | } 44 | 45 | func addContractsEventAbi(contracts []common.Address) { 46 | fmt.Printf("Automatically fetching ABIs from blockscanner for %v contracts (this might take a while, verbose flag -v for progress)\n", len(shared.Options.FilterAddresses)) 47 | // go through each tracked contract and fetch its ABI, adding it to 48 | // the event sigs data file 49 | shared.Infof(slog.Default(), "Fetching ABI of tracked contracts for event signatures") 50 | var abiStrings []string 51 | for _, contactAddress := range contracts { 52 | shared.Infof(slog.Default(), "Automatically fetching ABI for %v", contactAddress.Hex()) 53 | 54 | abiStr, err := database.GetAbiFromBlockscanner( 55 | shared.Blockscanners, 56 | shared.ChainID, 57 | contactAddress, 58 | ) 59 | if err != nil { 60 | shared.Warnf(slog.Default(), "%v", err) 61 | continue 62 | } 63 | abiStrings = append(abiStrings, abiStr) 64 | } 65 | // add each abi string to data file 66 | for _, abiString := range abiStrings { 67 | database.AddEventsFromAbi(abiString, "./data/event_sigs.json") 68 | } 69 | // reload the event sigs data 70 | shared.EventSigs = make(map[string]interface{}) 71 | shared.LoadFromJson("./data/event_sigs.json", &shared.EventSigs) 72 | fmt.Printf("Done fetching ABIs\n") 73 | } 74 | -------------------------------------------------------------------------------- /trackooor/blocklistener.go: -------------------------------------------------------------------------------- 1 | package trackooor 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "log/slog" 8 | "time" 9 | 10 | "github.com/Zellic/EVM-trackooor/actions" 11 | "github.com/Zellic/EVM-trackooor/shared" 12 | "github.com/Zellic/EVM-trackooor/utils" 13 | 14 | "github.com/ethereum/go-ethereum" 15 | "github.com/ethereum/go-ethereum/common" 16 | "github.com/ethereum/go-ethereum/core/types" 17 | "github.com/ethereum/go-ethereum/event" 18 | "github.com/schollz/progressbar/v3" 19 | ) 20 | 21 | // handles all events emitted in a given block, 22 | func handleEventsFromBlock(block *types.Block, contracts []common.Address) { 23 | // blockHash := block.Hash() 24 | query := ethereum.FilterQuery{ 25 | // BlockHash: &blockHash, 26 | FromBlock: block.Number(), 27 | ToBlock: block.Number(), 28 | Addresses: contracts, 29 | Topics: shared.Options.FilterEventTopics, 30 | } 31 | logs, err := shared.Client.FilterLogs(context.Background(), query) 32 | if err != nil { 33 | log.Fatal(err) 34 | } 35 | 36 | var bar *progressbar.ProgressBar 37 | if shared.Verbose { 38 | bar = progressbar.Default(int64(len(logs)), "Handling events") 39 | } 40 | for _, log := range logs { 41 | handleEventGeneral(log) 42 | if shared.Verbose { 43 | bar.Add(1) 44 | } 45 | } 46 | } 47 | 48 | // call transaction action function, if exists 49 | func doTxPostProcessing(block *types.Block) { 50 | var bar *progressbar.ProgressBar 51 | if shared.Verbose { 52 | bar = progressbar.Default(int64(len(block.Transactions())), "Processing transactions") 53 | } 54 | for _, tx := range block.Transactions() { 55 | to := tx.To() 56 | from := utils.GetTxSender(tx) 57 | 58 | // send tx data to post processing 59 | p := actions.ActionTxData{ 60 | Transaction: tx, 61 | Block: block, 62 | From: from, 63 | To: to, 64 | } 65 | // check tx `from` address, if no match then check `to` address 66 | actions.ActionMapMutex.RLock() 67 | if functions, ok := actions.TxAddressToAction[*p.From]; ok { 68 | // loop through and call all action funcs associated with that address 69 | for _, function := range functions { 70 | go function(p) 71 | } 72 | } else if p.To != nil { // in case tx was deployment, `To` will be null 73 | if functions, ok := actions.TxAddressToAction[*p.To]; ok { 74 | for _, function := range functions { 75 | go function(p) 76 | } 77 | } 78 | } 79 | actions.ActionMapMutex.RUnlock() 80 | 81 | if shared.Verbose { 82 | bar.Add(1) 83 | } 84 | } 85 | } 86 | 87 | func doBlocksPostProcessing(p actions.ActionBlockData) { 88 | for _, function := range actions.BlockActions { 89 | go function(p) 90 | } 91 | } 92 | 93 | func HandleHeader(header *types.Header) { 94 | shared.Infof(slog.Default(), "Handling header for block %v\n", header.Number) 95 | 96 | // make sure the event is not already processed when switching historical -> realtime 97 | if shared.SwitchingToRealtime { 98 | // check if already processed 99 | if entity, ok := shared.AlreadyProcessed[header.Hash()]; ok { 100 | if entity.BlockNumber.Cmp(header.Number) == 0 { 101 | // block was already processed, dont process again 102 | shared.DupeDetected = true 103 | fmt.Printf("Duplicate block detected, ignoring\n") 104 | return 105 | } 106 | } 107 | // otherwise add to map 108 | shared.AlreadyProcessed[header.Hash()] = shared.ProcessedEntity{BlockNumber: header.Number} 109 | } 110 | 111 | shared.Infof(slog.Default(), "Getting block by hash %v", header.Hash()) 112 | 113 | if shared.Options.IsL2Chain { 114 | // try getting block by hash, otherwise by number 115 | blockHexNum := "0x" + header.Number.Text(16) 116 | block, err := shared.GetL2BlockByHexNumber(blockHexNum) 117 | if err != nil { 118 | shared.Warnf(slog.Default(), "Failed to get block by hex num: %v, ignoring block, err: %v", blockHexNum, err) 119 | return 120 | } 121 | handleBlock(block) 122 | } else { 123 | var block *types.Block 124 | var err error 125 | 126 | if shared.Options.GetBlockByNumber { 127 | // try getting block by hash, otherwise by number 128 | block, err = shared.Client.BlockByNumber(context.Background(), header.Number) 129 | if err != nil { 130 | shared.Warnf(slog.Default(), "Failed to get block by number %v, getting by hash %v, err: %v", header.Hash(), header.Number, err) 131 | block, err = shared.Client.BlockByHash(context.Background(), header.Hash()) 132 | if err != nil { 133 | shared.Warnf(slog.Default(), "Failed to get block by hash %v, ignoring block, err: %v", header.Number, err) 134 | return 135 | } 136 | } 137 | } else { 138 | // try getting block by hash, otherwise by number 139 | block, err = shared.Client.BlockByHash(context.Background(), header.Hash()) 140 | if err != nil { 141 | shared.Warnf(slog.Default(), "Failed to get block by hash %v, getting by number %v, err: %v", header.Hash(), header.Number, err) 142 | block, err = shared.Client.BlockByNumber(context.Background(), header.Number) 143 | if err != nil { 144 | shared.Warnf(slog.Default(), "Failed to get block by number %v, ignoring block, err: %v", header.Number, err) 145 | return 146 | } 147 | } 148 | } 149 | 150 | handleBlock(block) 151 | } 152 | } 153 | 154 | func handleBlock(block *types.Block) { 155 | shared.Infof(slog.Default(), "Handling block %v\n", block.Number()) 156 | 157 | // call block post processing functions 158 | doBlocksPostProcessing(actions.ActionBlockData{Block: block}) 159 | 160 | // handle all events emitted in the block 161 | actions.ActionMapMutex.RLock() 162 | if len(actions.EventSigToAction) == 0 && len(actions.ContractToEventSigToAction) == 0 { 163 | // dont process if no event sigs mapped to action funcs 164 | actions.ActionMapMutex.RUnlock() 165 | shared.Infof(slog.Default(), "No event actions enabled - not handling") 166 | } else { 167 | actions.ActionMapMutex.RUnlock() 168 | handleEventsFromBlock(block, shared.Options.FilterAddresses) 169 | } 170 | 171 | // do tx post processing 172 | // dont process if no addresses mapped to action funcs 173 | actions.ActionMapMutex.RLock() 174 | if len(actions.TxAddressToAction) == 0 { 175 | actions.ActionMapMutex.RUnlock() 176 | shared.Infof(slog.Default(), "No tx address actions enabled - not handling") 177 | } else { 178 | actions.ActionMapMutex.RUnlock() 179 | doTxPostProcessing(block) 180 | } 181 | 182 | // wait for actions to finish (if the action requests it) 183 | if !shared.Options.HistoricalOptions.BatchFetchBlocks { 184 | shared.BlockWaitGroup.Wait() 185 | } 186 | } 187 | 188 | func ListenToBlocks() { 189 | 190 | headers := make(chan *types.Header) 191 | // resubscribe incase webhook goes down momentarily 192 | sub := event.Resubscribe(2*time.Second, func(ctx context.Context) (event.Subscription, error) { 193 | fmt.Printf("Subscribing to new head\n") 194 | return shared.Client.SubscribeNewHead(context.Background(), headers) 195 | }) 196 | 197 | // terminal output that we are starting 198 | logListeningBlocks() 199 | 200 | for { 201 | select { 202 | case err := <-sub.Err(): 203 | log.Fatalf("Error: %v\n", err) 204 | case header := <-headers: 205 | HandleHeader(header) 206 | } 207 | } 208 | } 209 | 210 | // PENDING BLOCKS LISTENER 211 | 212 | // whether or not a block num has been processed already 213 | var processedBlock map[string]bool 214 | 215 | func getPendingBlock() { 216 | // request pending block 217 | block, err := shared.GetL2BlockByHexNumber("pending") 218 | if err != nil { 219 | panic(err) 220 | } 221 | 222 | alreadyProcessed := processedBlock[block.Number().String()] 223 | if alreadyProcessed { 224 | // dont process again 225 | return 226 | } 227 | processedBlock[block.Number().String()] = true 228 | 229 | handleBlock(block) 230 | } 231 | 232 | func ListenToPendingBlocks() { 233 | 234 | processedBlock = make(map[string]bool) 235 | 236 | // terminal output that we are starting 237 | fmt.Printf("Warning: Listening to pending blocks\n") 238 | logListeningBlocks() 239 | 240 | // periodically query for pending block 241 | ticker := time.NewTicker(50 * time.Millisecond) 242 | quit := make(chan struct{}) 243 | for { 244 | select { 245 | case <-ticker.C: 246 | getPendingBlock() 247 | case <-quit: 248 | ticker.Stop() 249 | return 250 | } 251 | } 252 | } 253 | -------------------------------------------------------------------------------- /trackooor/eventlistener.go: -------------------------------------------------------------------------------- 1 | package trackooor 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "log/slog" 8 | "time" 9 | 10 | "github.com/Zellic/EVM-trackooor/actions" 11 | "github.com/Zellic/EVM-trackooor/shared" 12 | 13 | "github.com/ethereum/go-ethereum" 14 | "github.com/ethereum/go-ethereum/core/types" 15 | "github.com/ethereum/go-ethereum/event" 16 | ) 17 | 18 | // FUNCTIONS 19 | 20 | // forward data to action function if the action func mapping exists 21 | func doEventPostProcessing(vLog types.Log) { 22 | 23 | contract := vLog.Address // contract that emitted the event log 24 | if len(vLog.Topics) == 0 { 25 | return 26 | } 27 | eventSigHash := vLog.Topics[0] 28 | 29 | var eventFields shared.EventFields 30 | var decodedTopics map[string]interface{} 31 | var decodedData map[string]interface{} 32 | var actionEventData actions.ActionEventData 33 | 34 | // if event sig is recognised, decode event log 35 | if event, ok := shared.EventSigs[eventSigHash.Hex()]; ok { 36 | // decode event log 37 | event := event.(map[string]interface{}) 38 | 39 | eventFields, decodedTopics, decodedData = shared.DecodeTopicsAndData(vLog.Topics[1:], vLog.Data, event["abi"]) 40 | actionEventData = actions.ActionEventData{ 41 | EventLog: vLog, 42 | EventFields: eventFields, 43 | DecodedTopics: decodedTopics, 44 | DecodedData: decodedData, 45 | } 46 | } else { 47 | actionEventData = actions.ActionEventData{ 48 | EventLog: vLog, 49 | } 50 | } 51 | 52 | // call action funcs (if mappings exist) 53 | 54 | // check event sig -> func mapping 55 | actions.ActionMapMutex.RLock() 56 | if functions, ok := actions.EventSigToAction[eventSigHash]; ok { 57 | actions.ActionMapMutex.RUnlock() 58 | for _, function := range functions { 59 | go function(actionEventData) 60 | } 61 | } else { 62 | actions.ActionMapMutex.RUnlock() 63 | } 64 | 65 | // check contract address, event sig -> func mapping 66 | actions.ActionMapMutex.RLock() 67 | if functions, ok := actions.ContractToEventSigToAction[contract][eventSigHash]; ok { 68 | actions.ActionMapMutex.RUnlock() 69 | for _, function := range functions { 70 | go function(actionEventData) 71 | } 72 | } else { 73 | actions.ActionMapMutex.RUnlock() 74 | } 75 | 76 | // check contract address -> event func mapping 77 | actions.ActionMapMutex.RLock() 78 | if functions, ok := actions.ContractToEventAction[contract]; ok { 79 | actions.ActionMapMutex.RUnlock() 80 | 81 | for _, function := range functions { 82 | go function(actionEventData) 83 | } 84 | } else { 85 | actions.ActionMapMutex.RUnlock() 86 | } 87 | } 88 | 89 | func handleEventGeneral(vLog types.Log) { 90 | // make sure the event is not already processed when switching historical -> realtime 91 | if shared.SwitchingToRealtime { 92 | // check if already processed 93 | if entity, ok := shared.AlreadyProcessed[vLog.TxHash]; ok { 94 | if vLog.Index == entity.EventIndex { 95 | // event was already processed, dont process again 96 | shared.DupeDetected = true 97 | fmt.Printf("Duplicate event detected, ignoring this event\n") 98 | return 99 | } 100 | } 101 | // otherwise add to map 102 | shared.AlreadyProcessed[vLog.TxHash] = shared.ProcessedEntity{EventIndex: vLog.Index} 103 | } 104 | 105 | // send to post processing (if mapping exists) 106 | doEventPostProcessing(vLog) 107 | } 108 | 109 | // listens to events of multiple contracts, but instead of creating an event subscription log 110 | // for each contract, it uses a single channel for all of them. 111 | // downside is that event sigs cannot be filtered for each contract individually 112 | // aka cannot filter only event X for contract A, and event Y for contract B 113 | func ListenToEventsSingle() { 114 | 115 | // create single filter query with all contract addresses, and the event topics to filter by (if any) 116 | query := ethereum.FilterQuery{ 117 | Addresses: shared.Options.FilterAddresses, 118 | Topics: shared.Options.FilterEventTopics, 119 | } 120 | 121 | // subscribe to events 122 | logs := make(chan types.Log) 123 | // resubscribe incase webhook goes down momentarily 124 | sub := event.Resubscribe(2*time.Second, func(ctx context.Context) (event.Subscription, error) { 125 | fmt.Printf("Subscribing to event logs\n") 126 | return shared.Client.SubscribeFilterLogs(context.Background(), query, logs) 127 | }) 128 | 129 | // terminal output that we are listening 130 | logListeningEventsSingle(shared.Options) 131 | 132 | // forever loop: handle event logs 133 | for { 134 | select { 135 | case err := <-sub.Err(): 136 | log.Fatal(err) 137 | case vLog := <-logs: 138 | if vLog.Removed { 139 | shared.Warnf(slog.Default(), "Log reverted due to chain reorganisation, shouldn't be handling this event: %v\n", vLog.TxHash.Hex()) 140 | } else { 141 | handleEventGeneral(vLog) 142 | } 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /trackooor/historical.go: -------------------------------------------------------------------------------- 1 | package trackooor 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "log/slog" 8 | "math/big" 9 | "sync" 10 | "time" 11 | 12 | "github.com/Zellic/EVM-trackooor/actions" 13 | "github.com/Zellic/EVM-trackooor/shared" 14 | 15 | "github.com/ethereum/go-ethereum" 16 | "github.com/ethereum/go-ethereum/common" 17 | "github.com/schollz/progressbar/v3" 18 | ) 19 | 20 | // default values 21 | var stepBlocks = big.NewInt(10000) 22 | var MaxRequestsPerSecond = 30 // max rpc requests per second 23 | 24 | // function called just before starting a historical processor 25 | func SetupHistorical() { 26 | if shared.Options.HistoricalOptions.StepBlocks != nil { 27 | stepBlocks = shared.Options.HistoricalOptions.StepBlocks 28 | } 29 | if shared.Options.MaxRequestsPerSecond != 0 { 30 | MaxRequestsPerSecond = shared.Options.MaxRequestsPerSecond 31 | } 32 | 33 | if shared.Options.HistoricalOptions.FromBlock == nil { 34 | log.Fatalf("from-block not set!") 35 | } 36 | // use latest block if toBlock not set 37 | if shared.Options.HistoricalOptions.ToBlock == nil { 38 | header, err := shared.Client.HeaderByNumber(context.Background(), nil) 39 | if err != nil { 40 | panic(err) 41 | } 42 | shared.Options.HistoricalOptions.ToBlock = header.Number 43 | } 44 | } 45 | 46 | func ProcessHistoricalEventsSingle(fromBlock *big.Int, toBlock *big.Int, eventTopics [][]common.Hash) { 47 | // progress bar 48 | var mainBar *progressbar.ProgressBar 49 | mainBar = progressbar.Default(big.NewInt(0).Div(big.NewInt(0).Sub(toBlock, fromBlock), stepBlocks).Int64()) 50 | 51 | // loop through whole block range, requesting a certain amount of blocks at once 52 | for currentBlock := fromBlock; currentBlock.Cmp(toBlock) <= 0; currentBlock.Add(currentBlock, stepBlocks) { 53 | stepFrom := currentBlock 54 | // currentBlock + stepBlocks - 1 , due to block ranges being inclusive on both ends 55 | stepTo := big.NewInt(0).Add(currentBlock, big.NewInt(0).Sub(stepBlocks, big.NewInt(1))) 56 | // don't step past `toBlock` 57 | if stepTo.Cmp(toBlock) == 1 { 58 | stepTo = toBlock 59 | } 60 | 61 | // create single filter query with all contracts, using same event sig filter for all 62 | query := ethereum.FilterQuery{ 63 | Addresses: shared.Options.FilterAddresses, 64 | Topics: eventTopics, 65 | FromBlock: stepFrom, 66 | ToBlock: stepTo, 67 | } 68 | 69 | // request event logs filtered by filter log 70 | logs, err := shared.Client.FilterLogs(context.Background(), query) 71 | if err != nil { 72 | log.Fatal(err) 73 | } 74 | 75 | // handle returned logs 76 | var bar *progressbar.ProgressBar 77 | if shared.Verbose { 78 | bar = progressbar.Default(int64(len(logs)), fmt.Sprintf("%v events from blocks %v-%v", len(logs), stepFrom, stepTo)) 79 | } 80 | for _, log := range logs { 81 | handleEventGeneral(log) 82 | 83 | // handle request throughput 84 | if actions.RpcWaitGroup.GetCount() >= MaxRequestsPerSecond { 85 | time.Sleep(time.Second) 86 | actions.RpcWaitGroup.Wait() 87 | } 88 | if shared.Verbose { 89 | bar.Add(1) 90 | } 91 | } 92 | mainBar.Add(1) 93 | } 94 | } 95 | 96 | func ProcessHistoricalBlocks(fromBlock *big.Int, toBlock *big.Int) { 97 | // progress bar 98 | var mainBar *progressbar.ProgressBar 99 | mainBar = progressbar.Default(big.NewInt(0).Sub(toBlock, fromBlock).Int64()) 100 | 101 | if shared.Options.HistoricalOptions.BatchFetchBlocks { 102 | // process one block at a time 103 | if shared.Options.HistoricalOptions.LoopBackwards { 104 | log.Fatalf("unimplemented LoopBackwards & BatchFetchBlocks") 105 | } else { 106 | batchCount := big.NewInt(100) 107 | zero := big.NewInt(0) 108 | 109 | var blockNumWaitGroup sync.WaitGroup 110 | for currentBlock := fromBlock; currentBlock.Cmp(toBlock) <= 0; currentBlock.Add(currentBlock, big.NewInt(1)) { 111 | mainBar.Describe(fmt.Sprintf("Block %v", currentBlock)) 112 | 113 | blockNumWaitGroup.Add(1) 114 | shared.BlockWaitGroup.Add(1) 115 | go func() { 116 | tmpCurBlock := big.NewInt(0).Set(currentBlock) 117 | blockNumWaitGroup.Done() 118 | 119 | block, err := shared.Client.BlockByNumber(context.Background(), tmpCurBlock) 120 | if err != nil { 121 | log.Fatalf("Could not retrieve block %v, err: %v\n", tmpCurBlock, err) 122 | } 123 | 124 | handleBlock(block) 125 | }() 126 | 127 | blockNumWaitGroup.Wait() 128 | 129 | if big.NewInt(0).Mod(currentBlock, batchCount).Cmp(zero) == 0 { 130 | // fmt.Printf("currentBlock: %v, waiting for BlockWaitGroup...\n", currentBlock) // DEBUG 131 | shared.BlockWaitGroup.Wait() 132 | } 133 | 134 | mainBar.Add(1) 135 | } 136 | } 137 | } else { 138 | // process one block at a time 139 | if shared.Options.HistoricalOptions.LoopBackwards { 140 | shared.Infof(slog.Default(), "Looping backwards from %v to %v\n", fromBlock, toBlock) 141 | mainBar = progressbar.Default(big.NewInt(0).Sub(fromBlock, toBlock).Int64()) 142 | for currentBlock := fromBlock; currentBlock.Cmp(toBlock) >= 0; currentBlock.Sub(currentBlock, big.NewInt(1)) { 143 | mainBar.Describe(fmt.Sprintf("Block %v", currentBlock)) 144 | 145 | block, err := shared.Client.BlockByNumber(context.Background(), currentBlock) 146 | if err != nil { 147 | log.Fatalf("Could not retrieve block %v, err: %v\n", currentBlock, err) 148 | } 149 | // fmt.Printf("Retrieved block %v\n", block.Number()) 150 | 151 | handleBlock(block) 152 | 153 | mainBar.Add(1) 154 | } 155 | } else { 156 | for currentBlock := fromBlock; currentBlock.Cmp(toBlock) <= 0; currentBlock.Add(currentBlock, big.NewInt(1)) { 157 | mainBar.Describe(fmt.Sprintf("Block %v", currentBlock)) 158 | 159 | block, err := shared.Client.BlockByNumber(context.Background(), currentBlock) 160 | if err != nil { 161 | log.Fatalf("Could not retrieve block %v, err: %v\n", currentBlock, err) 162 | } 163 | 164 | handleBlock(block) 165 | 166 | mainBar.Add(1) 167 | } 168 | } 169 | } 170 | } 171 | 172 | func GetPastEventsSingle() { 173 | 174 | // output starting message to terminal 175 | logHistoricalEventsSingle() 176 | 177 | // block range (make new variable to prevent actual values from being modified as they are passed as pointers) 178 | fromBlock := big.NewInt(0).Set(shared.Options.HistoricalOptions.FromBlock) 179 | toBlock := big.NewInt(0).Set(shared.Options.HistoricalOptions.ToBlock) 180 | 181 | ProcessHistoricalEventsSingle(fromBlock, toBlock, shared.Options.FilterEventTopics) 182 | 183 | // TODO figure if want to keep continue to realtime or leave that up to actions 184 | // // if continuing to process realtime 185 | // if options.HistoricalOptions.ContinueToRealtime { 186 | // // note: post processor finished functions will not be called 187 | // switchToRealtimeEventsSingle(eventSigHashes) 188 | // } 189 | 190 | shared.BlockWaitGroup.Wait() 191 | shared.Infof(slog.Default(), "Running post processing finished functions") 192 | actions.FinishActions(shared.Options.Actions) 193 | } 194 | 195 | func GetPastBlocks() { 196 | 197 | // output starting msg to terminal 198 | logHistoricalBlocks() 199 | 200 | // block range (make new variable to prevent actual values from being modified as they are passed as pointers) 201 | fromBlock := big.NewInt(0).Set(shared.Options.HistoricalOptions.FromBlock) 202 | toBlock := big.NewInt(0).Set(shared.Options.HistoricalOptions.ToBlock) 203 | 204 | ProcessHistoricalBlocks(fromBlock, toBlock) 205 | 206 | // TODO figure if want to keep continue to realtime or leave that up to actions 207 | // // if continuing to process realtime 208 | // if shared.Options.HistoricalOptions.ContinueToRealtime { 209 | // switchToRealtimeBlocks() 210 | // } 211 | 212 | shared.BlockWaitGroup.Wait() 213 | shared.Infof(slog.Default(), "Running post processing finished functions") 214 | actions.FinishActions(shared.Options.Actions) 215 | } 216 | -------------------------------------------------------------------------------- /trackooor/logging.go: -------------------------------------------------------------------------------- 1 | package trackooor 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/Zellic/EVM-trackooor/shared" 7 | ) 8 | 9 | func logListeningEventsSingle(options shared.TrackooorOptions) { 10 | // terminal 11 | fmt.Printf("\n") 12 | fmt.Printf("Listening to events (single subscription log) on chain ID %v\n", shared.ChainID) 13 | fmt.Printf("\n") 14 | // don't output all tracked addresses if too many 15 | if len(options.FilterAddresses) > 30 { 16 | fmt.Printf("Filtering by %v contract addresses\n", len(options.FilterAddresses)) 17 | } else { 18 | fmt.Printf("Filtering by contract addresses (%v): %v\n", 19 | len(options.FilterAddresses), 20 | options.FilterAddresses, 21 | ) 22 | } 23 | fmt.Printf("Filtering by event topics: %v\n", options.FilterEventTopics) 24 | } 25 | 26 | func logListeningBlocks() { 27 | fmt.Printf("\n") 28 | fmt.Printf("Listening to blocks mined on chain ID %v\n", shared.ChainID) 29 | fmt.Printf("\n") 30 | // don't output all tracked addresses if too many 31 | if len(shared.Options.FilterAddresses) > 30 { 32 | fmt.Printf("Filtering by %v addresses\n", len(shared.Options.FilterAddresses)) 33 | } else { 34 | fmt.Printf( 35 | "Filtering by addresses (%v): %v\n", 36 | len(shared.Options.FilterAddresses), 37 | shared.Options.FilterAddresses, 38 | ) 39 | } 40 | fmt.Printf("\n") 41 | } 42 | 43 | func logHistoricalEventsSingle() { 44 | fmt.Printf("\n") 45 | fmt.Printf("Starting historical events processor - single subscription log\n") 46 | fmt.Printf("Block range: %v to %v (stepping %v at a time)\n", 47 | shared.Options.HistoricalOptions.FromBlock, 48 | shared.Options.HistoricalOptions.ToBlock, 49 | stepBlocks, 50 | ) 51 | fmt.Printf("\n") 52 | // don't output all tracked addresses if too many 53 | if len(shared.Options.FilterAddresses) > 30 { 54 | fmt.Printf("Filtering by %v contract addresses\n", len(shared.Options.FilterAddresses)) 55 | } else { 56 | fmt.Printf("Filtering by contract addresses (%v): %v\n", 57 | len(shared.Options.FilterAddresses), 58 | shared.Options.FilterAddresses, 59 | ) 60 | } 61 | fmt.Printf("Filtering by event topics (%v): %v\n", 62 | len(shared.Options.FilterEventTopics), 63 | shared.Options.FilterEventTopics, 64 | ) 65 | fmt.Printf("Max RPC requests per second: %v\n", MaxRequestsPerSecond) 66 | fmt.Printf("\n") 67 | } 68 | 69 | func logHistoricalBlocks() { 70 | fmt.Printf("\n") 71 | fmt.Printf("Starting historical blocks processor\n") 72 | fmt.Printf("Block range: %v to %v\n", 73 | shared.Options.HistoricalOptions.FromBlock, 74 | shared.Options.HistoricalOptions.ToBlock, 75 | ) 76 | fmt.Printf("\n") 77 | // don't output all tracked addresses if too many 78 | if len(shared.Options.FilterAddresses) > 30 { 79 | fmt.Printf("Filtering by %v addresses\n", len(shared.Options.FilterAddresses)) 80 | } else { 81 | fmt.Printf( 82 | "Filtering by addresses (%v): %v\n", 83 | len(shared.Options.FilterAddresses), 84 | shared.Options.FilterAddresses, 85 | ) 86 | } 87 | fmt.Printf("Filtering by event topics (%v): %v\n", 88 | len(shared.Options.FilterEventTopics), 89 | shared.Options.FilterEventTopics, 90 | ) 91 | fmt.Printf("Max RPC requests per second: %v\n", MaxRequestsPerSecond) 92 | fmt.Printf("\n") 93 | } 94 | -------------------------------------------------------------------------------- /utils/formatting.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "log/slog" 6 | "math/big" 7 | "reflect" 8 | "strings" 9 | 10 | "github.com/Zellic/EVM-trackooor/shared" 11 | 12 | "github.com/ethereum/go-ethereum/common" 13 | ) 14 | 15 | // formats n given its decimal places. 16 | // truncate to 2 decimal places if formatted number > 0 17 | // returns 0 if number == 0 18 | func FormatDecimals(n *big.Int, decimals uint8) string { 19 | str := n.String() 20 | if decimals == 0 || n == nil || n.Cmp(big.NewInt(0)) == 0 { 21 | // dont format if no decimals or number is nil or number is 0 22 | return str 23 | } 24 | decs := int(decimals) 25 | l := len(str) 26 | if l > int(decimals) { 27 | pos := l - decs 28 | if decs > 2 { 29 | return str[:pos] + "." + str[pos:pos+2] 30 | } 31 | return str[:pos] + "." + str[pos:] 32 | } 33 | return strings.TrimRight("0."+strings.Repeat("0", decs-l)+str, "0") 34 | } 35 | 36 | // format into block scanner URL, like etherscan 37 | // should return unformatted value if blockscanner data for it doesnt exist 38 | // `typ` - type of entity, such as address, block, tx, token 39 | func BlockscanFormat(typ string, value string) string { 40 | chain, ok := shared.Blockscanners[shared.ChainID.String()] 41 | if !ok { 42 | return value 43 | } 44 | chainMap := chain.(map[string]interface{}) 45 | url, ok := chainMap[typ] 46 | if !ok { 47 | return value 48 | } 49 | urlStr := url.(string) 50 | return fmt.Sprintf(urlStr, value) 51 | } 52 | 53 | func CodeQuote(s string) string { 54 | if s == "" { 55 | return s 56 | } 57 | return "``" + s + "``" 58 | } 59 | 60 | func TrimFirstInstance(s, prefix string) string { 61 | if strings.HasPrefix(s, prefix) { 62 | return s[len(prefix):] 63 | } 64 | return s 65 | } 66 | 67 | func FormatBlockscanHyperlink(typ string, displayValue string, value string) string { 68 | url := BlockscanFormat(typ, value) 69 | // dont format if blockscan didn't format 70 | if url == value { 71 | return displayValue 72 | } 73 | return fmt.Sprintf("[`%v`](<%v>)", displayValue, url) 74 | } 75 | 76 | func FormatEventInfo(eventFields shared.EventFields, topics map[string]interface{}, data map[string]interface{}) (string, string) { 77 | // TODO handle structs for normal string output 78 | 79 | var output string 80 | var discordOutput string 81 | for _, eventField := range eventFields { 82 | var fieldValueFormatted string 83 | var fieldValueDiscordFormatted string 84 | fieldName, fieldType := eventField.FieldName, eventField.FieldType 85 | 86 | // find the value, either in topics or data 87 | fieldValue, ok := topics[fieldName] 88 | if !ok { 89 | fieldValue, ok = data[fieldName] 90 | if !ok { 91 | shared.Warnf(slog.Default(), "Couldn't find field value for field '%v'", fieldName) 92 | continue 93 | } 94 | } 95 | 96 | // convert field value to readable format 97 | 98 | if strings.HasPrefix(fieldType, "bytes") && !strings.HasSuffix(fieldType, "]") { // dynamic or static length bytes 99 | if fieldType == "bytes" && eventField.Indexed { 100 | // if indexed, value will be hash of the bytes 101 | fieldValueFormatted = fmt.Sprintf( 102 | "%v (keccak hash)", 103 | fieldValue, 104 | ) 105 | fieldValueDiscordFormatted = fmt.Sprintf( 106 | "%v (keccak hash)", 107 | CodeQuote(fieldValue.(common.Hash).Hex()), 108 | ) 109 | } else { 110 | // convert fixed byte array interface to []byte 111 | fieldValueLength := reflect.ValueOf(fieldValue).Len() 112 | decodedData := make([]byte, fieldValueLength) 113 | for i := 0; i < fieldValueLength; i++ { 114 | decodedData[i] = byte(reflect.ValueOf(fieldValue).Index(i).Uint()) 115 | } 116 | fieldValueFormatted = fmt.Sprintf( 117 | "%v (UTF-8: %v)", 118 | "0x"+common.Bytes2Hex(decodedData), 119 | string(decodedData), 120 | ) 121 | fieldValueDiscordFormatted = fmt.Sprintf( 122 | "`%v` (UTF-8: %v)", 123 | "0x"+common.Bytes2Hex(decodedData), 124 | CodeQuote(string(decodedData)), 125 | ) 126 | } 127 | } else if fieldType == "string" { 128 | if eventField.Indexed { 129 | // if indexed, value will be hash of the string 130 | fieldValueFormatted = fmt.Sprintf( 131 | "%v (keccak hash)", 132 | fieldValue, 133 | ) 134 | fieldValueDiscordFormatted = fmt.Sprintf( 135 | "%v (keccak hash)", 136 | CodeQuote(fieldValue.(common.Hash).Hex()), 137 | ) 138 | } else { 139 | fieldValueFormatted = fmt.Sprintf("%v", fieldValue) 140 | fieldValueDiscordFormatted = fmt.Sprintf("%v", CodeQuote(fieldValue.(string))) 141 | } 142 | } else { 143 | fieldValueFormatted = fmt.Sprintf("%v", fieldValue) 144 | if fieldType == "address" { 145 | fieldValueDiscordFormatted = FormatBlockscanHyperlink( 146 | "address", 147 | fieldValue.(common.Address).Hex(), 148 | fieldValue.(common.Address).Hex(), 149 | ) 150 | } else { 151 | fieldValueDiscordFormatted = fmt.Sprintf("`%v`", fieldValue) 152 | } 153 | } 154 | output += fmt.Sprintf( 155 | "%v: %v\n", 156 | fieldName, 157 | fieldValueFormatted, 158 | ) 159 | discordOutput += fmt.Sprintf( 160 | "%v: %v\n", 161 | fieldName, 162 | fieldValueDiscordFormatted, 163 | ) 164 | } 165 | return output, discordOutput 166 | } 167 | 168 | // makes addresses hex shorter 169 | // e.g. 0xf05285270B723f389803cf6dCf15d253d087782b becomes 0xf0528...7782b 170 | func ShortenAddress(addr common.Address) string { 171 | hexAddr := addr.Hex() 172 | return hexAddr[:2+5] + "..." + hexAddr[len(hexAddr)-5:] 173 | } 174 | 175 | // make hashes, e.g tx hashes shorter 176 | func ShortenHash(hsh common.Hash) string { 177 | hshHex := hsh.Hex() 178 | return hshHex[:2+10] + "..." + hshHex[len(hshHex)-10:] 179 | } 180 | -------------------------------------------------------------------------------- /utils/math.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "math/big" 7 | ) 8 | 9 | func Average(values []*big.Int) *big.Int { 10 | total := big.NewInt(0) 11 | for _, v := range values { 12 | total.Add(total, v) 13 | } 14 | return big.NewInt(0).Div(total, big.NewInt(int64(len(values)))) 15 | } 16 | 17 | func Sum(values []*big.Int) *big.Int { 18 | sum := big.NewInt(0) 19 | for _, v := range values { 20 | sum.Add(sum, v) 21 | } 22 | return sum 23 | } 24 | 25 | // BigInt is a wrapper for big.Int to implement custom JSON marshaling/unmarshaling. 26 | type BigInt struct { 27 | Int *big.Int `json:"int"` 28 | } 29 | 30 | // MarshalJSON marshals the BigInt into JSON, converting it to a string. 31 | func (b BigInt) MarshalJSON() ([]byte, error) { 32 | return json.Marshal(b.Int.String()) // Convert to string 33 | } 34 | 35 | // UnmarshalJSON unmarshals a JSON string into BigInt. 36 | func (b *BigInt) UnmarshalJSON(data []byte) error { 37 | var str string 38 | if err := json.Unmarshal(data, &str); err != nil { 39 | return err 40 | } 41 | intValue, success := new(big.Int).SetString(str, 10) // Parse from base 10 string 42 | if !success { 43 | return fmt.Errorf("failed to unmarshal BigInt: %s", str) 44 | } 45 | b.Int = intValue 46 | return nil 47 | } 48 | --------------------------------------------------------------------------------