diff --git a/go.mod b/go.mod index ee4eb77..2993186 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/labstack/gommon v0.4.0 github.com/mozillazg/go-unidecode v0.2.0 github.com/stretchr/testify v1.7.0 - github.com/tonkeeper/tongo v1.2.2 + golang.org/x/exp v0.0.0-20230116083435-1de6713980de golang.org/x/text v0.7.0 gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b ) @@ -16,12 +16,8 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/mattn/go-colorable v0.1.11 // indirect github.com/mattn/go-isatty v0.0.14 // indirect - github.com/oasisprotocol/curve25519-voi v0.0.0-20220328075252-7dd334e3daae // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/snksoft/crc v1.1.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasttemplate v1.2.1 // indirect - golang.org/x/crypto v0.0.0-20220321153916-2c7772ba3064 // indirect - golang.org/x/exp v0.0.0-20230116083435-1de6713980de // indirect golang.org/x/sys v0.1.0 // indirect ) diff --git a/go.sum b/go.sum index 34076b6..42ff765 100644 --- a/go.sum +++ b/go.sum @@ -11,23 +11,15 @@ github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9 github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mozillazg/go-unidecode v0.2.0 h1:vFGEzAH9KSwyWmXCOblazEWDh7fOkpmy/Z4ArmamSUc= github.com/mozillazg/go-unidecode v0.2.0/go.mod h1:zB48+/Z5toiRolOZy9ksLryJ976VIwmDmpQ2quyt1aA= -github.com/oasisprotocol/curve25519-voi v0.0.0-20220328075252-7dd334e3daae h1:7smdlrfdcZic4VfsGKD2ulWL804a4GVphr4s7WZxGiY= -github.com/oasisprotocol/curve25519-voi v0.0.0-20220328075252-7dd334e3daae/go.mod h1:hVoHR2EVESiICEMbg137etN/Lx+lSrHPTD39Z/uE+2s= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/snksoft/crc v1.1.0 h1:HkLdI4taFlgGGG1KvsWMpz78PkOC9TkPVpTV/cuWn48= -github.com/snksoft/crc v1.1.0/go.mod h1:5/gUOsgAm7OmIhb6WJzw7w5g2zfJi4FrHYgGPdshE+A= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/tonkeeper/tongo v1.2.2 h1:bihFqAFOXGnub59YyoXgdPq1iw9VYwdOXznG9Vz2D4M= -github.com/tonkeeper/tongo v1.2.2/go.mod h1:LdOBjpUz6vLp1EdX3E0XLNks9YI5XMSqaQahfOMrBEY= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasttemplate v1.2.1 h1:TVEnxayobAdVkhQfrfes2IzOB6o+z4roRkPF52WA1u4= github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= -golang.org/x/crypto v0.0.0-20220321153916-2c7772ba3064 h1:S25/rfnfsMVgORT4/J61MJ7rdyseOZOyvLIrZEZ7s6s= -golang.org/x/crypto v0.0.0-20220321153916-2c7772ba3064/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20230116083435-1de6713980de h1:DBWn//IJw30uYCgERoxCg84hWtA97F4wMiKOIh00Uf0= golang.org/x/exp v0.0.0-20230116083435-1de6713980de/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/jetton_verifier.go b/jetton_verifier.go index ca812fc..12dd648 100644 --- a/jetton_verifier.go +++ b/jetton_verifier.go @@ -1,9 +1,14 @@ package scam_backoffice_rules import ( + "encoding/base64" + "encoding/binary" + "encoding/hex" "encoding/json" "fmt" "net/http" + "strconv" + "strings" "sync" "time" "unicode" @@ -12,7 +17,6 @@ import ( "github.com/avast/retry-go" "github.com/labstack/gommon/log" - "github.com/tonkeeper/tongo" ) const jettonPath = "https://raw.githubusercontent.com/tonkeeper/ton-assets/main/jettons.json" @@ -23,13 +27,18 @@ const jettonPath = "https://raw.githubusercontent.com/tonkeeper/ton-assets/main/ type JettonVerifier struct { // mu protects Jettons mu sync.RWMutex - jettons map[string]map[tongo.AccountID]jetton + jettons map[string]map[accountID]jetton } type jetton struct { - Name string `json:"name"` - Address tongo.AccountID `json:"address"` - Symbol string `json:"symbol"` + Name string `json:"name"` + Address accountID `json:"address"` + Symbol string `json:"symbol"` +} + +type accountID struct { + workchain int32 + address [32]byte } // hardcodedBlacklistedSymbols contains symbols that are known to be used by scam jettons. @@ -132,7 +141,7 @@ var manuallyWhitelisted = &unicode.RangeTable{ func NewJettonVerifier() *JettonVerifier { verifier := JettonVerifier{ // we have valid jettons sharing the same symbol - jettons: map[string]map[tongo.AccountID]jetton{}, + jettons: map[string]map[accountID]jetton{}, } go verifier.run() return &verifier @@ -155,11 +164,11 @@ func (verifier *JettonVerifier) run() { } func (verifier *JettonVerifier) updateJettons(knownJettons []jetton) { - jettons := make(map[string]map[tongo.AccountID]jetton, len(knownJettons)) + jettons := make(map[string]map[accountID]jetton, len(knownJettons)) for _, item := range knownJettons { normalized := NormalizeString(item.Symbol) if _, ok := jettons[normalized]; !ok { - jettons[normalized] = make(map[tongo.AccountID]jetton) + jettons[normalized] = make(map[accountID]jetton) } jettons[normalized][item.Address] = item } @@ -169,7 +178,7 @@ func (verifier *JettonVerifier) updateJettons(knownJettons []jetton) { } // IsBlacklisted returns true if the jetton SYMBOL is similar to any of the well-known jettons. -func (verifier *JettonVerifier) IsBlacklisted(address tongo.AccountID, symbol string) bool { +func (verifier *JettonVerifier) IsBlacklisted(workchain int32, address [32]byte, symbol string) bool { for _, s := range symbol { // if the symbol contains non-printable characters, // we consider it a scam. @@ -197,13 +206,26 @@ func (verifier *JettonVerifier) IsBlacklisted(address tongo.AccountID, symbol st // no jettons with such symbol return false } - if _, ok := jettons[address]; ok { + if _, ok := jettons[accountID{workchain: workchain, address: address}]; ok { // this jetton is in our list of well-known jettons return false } return true } +func (id *accountID) UnmarshalJSON(data []byte) error { + var s string + if err := json.Unmarshal(data, &s); err != nil { + return err + } + parsed, err := parseAccountID(s) + if err != nil { + return err + } + *id = parsed + return nil +} + func SetBlacklistedSymbols(blacklistedSymbols []string) { hardcodedBlacklistedSymbolsMutex.Lock() hardcodedBlacklistedSymbols = blacklistedSymbols @@ -225,3 +247,60 @@ func downloadJettons() ([]jetton, error) { } return jettons, nil } + +// parseAccountID accepts the raw form "workchain:hex" and the base64 user-friendly form. +func parseAccountID(s string) (accountID, error) { + s = strings.TrimSpace(s) + if workchain, hash, ok := strings.Cut(s, ":"); ok { + return parseRawAccountID(workchain, hash) + } + return parseFriendlyAccountID(s) +} + +func parseRawAccountID(workchain, hash string) (accountID, error) { + var id accountID + wc, err := strconv.ParseInt(workchain, 10, 32) + if err != nil { + return accountID{}, fmt.Errorf("invalid workchain %q: %w", workchain, err) + } + id.workchain = int32(wc) + b, err := hex.DecodeString(hash) + if err != nil || len(b) != len(id.address) { + return accountID{}, fmt.Errorf("invalid account hash %q", hash) + } + copy(id.address[:], b) + return id, nil +} + +func parseFriendlyAccountID(s string) (accountID, error) { + var id accountID + b, err := base64.URLEncoding.DecodeString(s) + if err != nil { + b, err = base64.StdEncoding.DecodeString(s) + } + if err != nil || len(b) != 36 { + return accountID{}, fmt.Errorf("invalid account address %q", s) + } + if crc16(b[:34]) != binary.BigEndian.Uint16(b[34:]) { + return accountID{}, fmt.Errorf("invalid account address %q: checksum mismatch", s) + } + id.workchain = int32(int8(b[1])) + copy(id.address[:], b[2:34]) + return id, nil +} + +// crc16 is CRC-16/XMODEM, the checksum of user-friendly addresses. +func crc16(data []byte) uint16 { + var crc uint16 + for _, b := range data { + crc ^= uint16(b) << 8 + for i := 0; i < 8; i++ { + if crc&0x8000 != 0 { + crc = crc<<1 ^ 0x1021 + } else { + crc <<= 1 + } + } + } + return crc +} diff --git a/jetton_verifier_test.go b/jetton_verifier_test.go index 11b6ff1..6b5de44 100644 --- a/jetton_verifier_test.go +++ b/jetton_verifier_test.go @@ -5,8 +5,6 @@ import ( "testing" "github.com/stretchr/testify/require" - "github.com/tonkeeper/tongo" - "github.com/tonkeeper/tongo/ton" ) var ( @@ -14,26 +12,26 @@ var ( { Name: "Cock Fights Token", Symbol: "CFT", - Address: tongo.MustParseAccountID("0:a6e0456ba1ca77e0915e94760f1b1fc3e292aa43e812ebfc45650cc8c3003e58"), + Address: mustParseAccountID("0:a6e0456ba1ca77e0915e94760f1b1fc3e292aa43e812ebfc45650cc8c3003e58"), }, { Name: "jUSDT", Symbol: "jUSDT", - Address: tongo.MustParseAccountID("0:729c13b6df2c07cbf0a06ab63d34af454f3d320ec1bcd8fb5c6d24d0806a17c2"), + Address: mustParseAccountID("0:729c13b6df2c07cbf0a06ab63d34af454f3d320ec1bcd8fb5c6d24d0806a17c2"), }, { Name: "Valid duplicate of jUSDT", Symbol: "jUSDT", - Address: tongo.MustParseAccountID("-1:729c13b6df2c07cbf0a06ab63d34af454f3d320ec1bcd8fb5c6d24d0806a17c2"), + Address: mustParseAccountID("-1:729c13b6df2c07cbf0a06ab63d34af454f3d320ec1bcd8fb5c6d24d0806a17c2"), }, { Name: "Ambra", Symbol: "AMBR", - Address: tongo.MustParseAccountID("0:9c2c05b9dfb2a7460fda48fae7409a32623399933a98a7a15599152f37572b49"), + Address: mustParseAccountID("0:9c2c05b9dfb2a7460fda48fae7409a32623399933a98a7a15599152f37572b49"), }, { // this is a fake token added to the well-known jettons list to ban scam tokens - Address: tongo.MustParseAccountID("0:ca9006bd3fb03d355daeeff93b24be90afaa6e3ca0073ff5720f8a852c933278"), + Address: mustParseAccountID("0:ca9006bd3fb03d355daeeff93b24be90afaa6e3ca0073ff5720f8a852c933278"), Name: "Tether USD", Symbol: "USDT", }, @@ -45,7 +43,7 @@ func TestJettonVerifier_IsSimilarToWellKnownSymbol(t *testing.T) { name string tokenName string symbol string - address ton.AccountID + address accountID wantBlacklisted bool }{ { @@ -127,99 +125,99 @@ func TestJettonVerifier_IsSimilarToWellKnownSymbol(t *testing.T) { { name: "original jUSDC", symbol: "jUSDC", - address: ton.MustParseAccountID("0:7e30fc2b7751ba58a3642f3fd59d5e96a810ddd78d8a310bfe8353bef10500df"), + address: mustParseAccountID("0:7e30fc2b7751ba58a3642f3fd59d5e96a810ddd78d8a310bfe8353bef10500df"), wantBlacklisted: false, }, { name: "original jUSDT", symbol: "jUSDT", - address: ton.MustParseAccountID("0:729c13b6df2c07cbf0a06ab63d34af454f3d320ec1bcd8fb5c6d24d0806a17c2"), + address: mustParseAccountID("0:729c13b6df2c07cbf0a06ab63d34af454f3d320ec1bcd8fb5c6d24d0806a17c2"), wantBlacklisted: false, }, { name: "valid duplicate of jUSDT", symbol: "jUSDT", - address: ton.MustParseAccountID("-1:729c13b6df2c07cbf0a06ab63d34af454f3d320ec1bcd8fb5c6d24d0806a17c2"), + address: mustParseAccountID("-1:729c13b6df2c07cbf0a06ab63d34af454f3d320ec1bcd8fb5c6d24d0806a17c2"), wantBlacklisted: false, }, { name: "jUSDT but with different address", symbol: "jUSDT", - address: ton.MustParseAccountID("0:729c13b6df2c07cbf0a06ab63d34af454f3d320ec1bcd8fb5c6d24d0806a1700"), + address: mustParseAccountID("0:729c13b6df2c07cbf0a06ab63d34af454f3d320ec1bcd8fb5c6d24d0806a1700"), wantBlacklisted: true, }, { name: "jUSDT but with different address and non ascii symbols", symbol: "jUЅDT", - address: ton.MustParseAccountID("0:729c13b6df2c07cbf0a06ab63d34af454f3d320ec1bcd8fb5c6d24d0806a1700"), + address: mustParseAccountID("0:729c13b6df2c07cbf0a06ab63d34af454f3d320ec1bcd8fb5c6d24d0806a1700"), wantBlacklisted: true, }, { symbol: "jUSDT", - address: ton.MustParseAccountID("0:729c13b6df2c07cbf0a06ab63d34af454f3d320ec1bcd8fb5c6d24d0806a17c2"), + address: mustParseAccountID("0:729c13b6df2c07cbf0a06ab63d34af454f3d320ec1bcd8fb5c6d24d0806a17c2"), wantBlacklisted: false, }, { symbol: "CFT", - address: tongo.MustParseAccountID("0:a6e0456ba1ca77e0915e94760f1b1fc3e292aa43e812ebfc45650cc8c3003e58"), + address: mustParseAccountID("0:a6e0456ba1ca77e0915e94760f1b1fc3e292aa43e812ebfc45650cc8c3003e58"), wantBlacklisted: false, }, { symbol: "CFT", - address: tongo.AccountID{}, // emulate different address + address: accountID{}, // emulate different address wantBlacklisted: true, }, { symbol: "Random Symbol", - address: tongo.AccountID{}, + address: accountID{}, wantBlacklisted: false, }, { name: "valid cyrillic symbol", symbol: "Токен", - address: tongo.AccountID{}, + address: accountID{}, wantBlacklisted: false, }, { name: "valid German symbol", symbol: "genießen", - address: tongo.AccountID{}, + address: accountID{}, wantBlacklisted: false, }, { name: "valid Hindi symbol", symbol: "टोकन", - address: tongo.AccountID{}, + address: accountID{}, wantBlacklisted: false, }, { name: "valid Marathi symbol", symbol: "टोकन", - address: tongo.AccountID{}, + address: accountID{}, wantBlacklisted: false, }, { name: "valid Telugu symbol", symbol: "టోకెన్", - address: tongo.AccountID{}, + address: accountID{}, wantBlacklisted: false, }, { name: "valid symbol with typographic apostrophe", symbol: "Juli\u2019s Cat", - address: tongo.AccountID{}, + address: accountID{}, wantBlacklisted: false, }, { name: "valid symbol with left single quotation mark", symbol: "\u2018Token\u2019", - address: tongo.AccountID{}, + address: accountID{}, wantBlacklisted: false, }, { name: "valid symbol with typographic double quotes", symbol: "\u201CSmart\u201D", - address: tongo.AccountID{}, + address: accountID{}, wantBlacklisted: false, }, } @@ -227,9 +225,9 @@ func TestJettonVerifier_IsSimilarToWellKnownSymbol(t *testing.T) { t.Run(tt.name, func(t *testing.T) { verifier := &JettonVerifier{} verifier.updateJettons(testKnownJettons) - similar := verifier.IsBlacklisted(tt.address, tt.symbol) + similar := verifier.IsBlacklisted(tt.address.workchain, tt.address.address, tt.symbol) if tt.tokenName != "" { - similar = similar || verifier.IsBlacklisted(tt.address, tt.tokenName) + similar = similar || verifier.IsBlacklisted(tt.address.workchain, tt.address.address, tt.tokenName) } require.Equal(t, tt.wantBlacklisted, similar) @@ -239,7 +237,7 @@ func TestJettonVerifier_IsSimilarToWellKnownSymbol(t *testing.T) { func TestJettonVerifier_run(t *testing.T) { verifier := &JettonVerifier{ - jettons: map[string]map[tongo.AccountID]jetton{}, + jettons: map[string]map[accountID]jetton{}, } knownJettons, err := downloadJettons() require.Nil(t, err) @@ -251,3 +249,30 @@ func TestJettonVerifier_run(t *testing.T) { } require.Equal(t, 0, len(jettons)) } + +func TestParseAccountID(t *testing.T) { + raw := mustParseAccountID("0:b113a994b5024a16719f69139328eb759596c38a25f59028b146fecdc3621dfe") + for _, s := range []string{ + "EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs", + "EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id/sDs", + "UQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_p0p", + } { + id, err := parseAccountID(s) + require.NoError(t, err, s) + require.Equal(t, raw, id, s) + } + _, err := parseAccountID("EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDt") + require.Error(t, err) + _, err = parseAccountID("0:b113") + require.Error(t, err) + masterchain := mustParseAccountID("-1:729c13b6df2c07cbf0a06ab63d34af454f3d320ec1bcd8fb5c6d24d0806a17c2") + require.Equal(t, int32(-1), masterchain.workchain) +} + +func mustParseAccountID(s string) accountID { + id, err := parseAccountID(s) + if err != nil { + panic(err) + } + return id +}