Dolandırıcı token'ların kullandığı bazı hileler ve bunları tespit etme yöntemleri
Bu eğitimde, dolandırıcıların oynadığı bazı hileleri ve bunları nasıl uyguladıklarını görmek için dolandırıcı bir token'ı (opens in a new tab) inceliyoruz. Eğitimin sonunda, ERC-20 token sözleşmeleri, yetenekleri ve şüpheciliğin neden gerekli olduğu hakkında daha kapsamlı bir görüşe sahip olacaksınız. Ardından, bu dolandırıcı token tarafından yayımlanan olaylara bakıyor ve bunun meşru olmadığını otomatik olarak nasıl belirleyebileceğimizi görüyoruz.
Dolandırıcı token'lar - nedir, insanlar neden bunları yapar ve bunlardan nasıl kaçınılır
Ethereum'un en yaygın kullanımlarından biri, bir grubun ticareti yapılabilir bir token, bir anlamda kendi para birimini yaratmasıdır. Ancak, değer getiren meşru kullanım durumlarının olduğu her yerde, bu değeri kendileri için çalmaya çalışan suçlular da vardır.
Bu konu hakkında daha fazla bilgiyi kullanıcı perspektifinden ethereum.org'un başka bir yerinde okuyabilirsiniz. Bu eğitim, bunun nasıl yapıldığını ve nasıl tespit edilebileceğini görmek için dolandırıcı bir token'ı incelemeye odaklanmaktadır.
wARB'ın bir dolandırıcılık olduğunu nasıl anlarım?
İncelediğimiz token, meşru ARB token'ına (opens in a new tab) eşdeğermiş gibi davranan wARB (opens in a new tab)'tır.
Hangisinin meşru token olduğunu anlamanın en kolay yolu, onu oluşturan organizasyona, yani Arbitrum (opens in a new tab)'a bakmaktır. Meşru adresler kendi belgelerinde (opens in a new tab) belirtilmiştir.
Kaynak kodu neden mevcut?
Normalde başkalarını dolandırmaya çalışan kişilerin gizli olmasını bekleriz ve gerçekten de birçok dolandırıcı token'ın kodu mevcut değildir (örneğin, bu (opens in a new tab) ve bu (opens in a new tab)).
Ancak, meşru token'lar genellikle kaynak kodlarını yayımlar, bu nedenle meşru görünmek için dolandırıcı token yazarları da bazen aynısını yapar. wARB (opens in a new tab), kaynak kodu mevcut olan bu token'lardan biridir ve bu da onu anlamayı kolaylaştırır.
Sözleşme dağıtıcıları kaynak kodunu yayımlayıp yayımlamamayı seçebilseler de, yanlış kaynak kodunu yayımlayamazlar. Blok gezgini, sağlanan kaynak kodunu bağımsız olarak derler ve tam olarak aynı baytkodu elde edemezse, o kaynak kodunu reddeder. Bu konu hakkında daha fazla bilgiyi Etherscan sitesinde okuyabilirsiniz (opens in a new tab).
Meşru ERC-20 token'ları ile karşılaştırma
Bu token'ı meşru ERC-20 token'ları ile karşılaştıracağız. Meşru ERC-20 token'larının tipik olarak nasıl yazıldığına aşina değilseniz, bu eğitime bakın.
Ayrıcalıklı adresler için sabitler
Sözleşmeler bazen ayrıcalıklı adreslere ihtiyaç duyar. Uzun vadeli kullanım için tasarlanmış sözleşmeler, örneğin yeni bir çoklu imza sözleşmesinin kullanımını sağlamak için bazı ayrıcalıklı adreslerin bu adresleri değiştirmesine izin verir. Bunu yapmanın birkaç yolu vardır.
HOP token sözleşmesi (opens in a new tab), Ownable (opens in a new tab) modelini kullanır. Ayrıcalıklı adres, depolamada _owner adlı bir alanda tutulur (üçüncü dosyaya, Ownable.sol'ye bakın).
abstract contract Ownable is Context {
address private _owner;
.
.
.
}
ARB token sözleşmesinin (opens in a new tab) doğrudan ayrıcalıklı bir adresi yoktur. Ancak, buna ihtiyacı da yoktur. 0xb50721bcf8d664c30412cfbc6cf7a15145234ad1 adresindeki (opens in a new tab) bir proxy (opens in a new tab)'nin arkasında yer alır. Bu sözleşmenin yükseltmeler için kullanılabilecek ayrıcalıklı bir adresi vardır (dördüncü dosyaya, ERC1967Upgrade.sol'ye bakın).
/**
* @dev EIP1967 admin yuvasında yeni bir Adres depolar.
*/
function _setAdmin(address newAdmin) private {
require(newAdmin != address(0), "ERC1967: new admin is the zero address");
StorageSlot.getAddressSlot(_ADMIN_SLOT).value = newAdmin;
}
Buna karşılık, wARB sözleşmesinin koda gömülü (hard coded) bir contract_owner'i vardır.
contract WrappedArbitrum is Context, IERC20 {
.
.
.
address deployer = 0xB50721BCf8d664c30412Cfbc6cf7a15145234ad1;
address public contract_owner = 0xb40dE7b1beE84Ff2dc22B70a049A07A13a411A33;
.
.
.
}
Bu sözleşme sahibi (opens in a new tab), farklı zamanlarda farklı hesaplar tarafından kontrol edilebilecek bir sözleşme değil, harici olarak sahip olunan bir hesaptır. Bu, muhtemelen değerli kalacak bir ERC-20'yi kontrol etmek için uzun vadeli bir çözümden ziyade, bir birey tarafından kısa vadeli kullanım için tasarlandığı anlamına gelir.
Ve gerçekten de, Etherscan'e bakarsak, dolandırıcının bu sözleşmeyi 19 Mayıs 2023'te yalnızca 12 saat boyunca (ilk işlemden (opens in a new tab) son işleme (opens in a new tab) kadar) kullandığını görürüz.
Sahte _transfer işlevi
Gerçek transferlerin dahili bir _transfer işlevi kullanılarak gerçekleşmesi standarttır.
wARB içinde bu işlev neredeyse meşru görünür:
function _transfer(address sender, address recipient, uint256 amount) internal virtual{
require(sender != address(0), "ERC20: transfer from the zero address");
require(recipient != address(0), "ERC20: transfer to the zero address");
_beforeTokenTransfer(sender, recipient, amount);
_balances[sender] = _balances[sender].sub(amount, "ERC20: transfer amount exceeds balance");
_balances[recipient] = _balances[recipient].add(amount);
if (sender == contract_owner){
sender = deployer;
}
emit Transfer(sender, recipient, amount);
}
Şüpheli kısım şudur:
if (sender == contract_owner){
sender = deployer;
}
emit Transfer(sender, recipient, amount);
Sözleşme sahibi token gönderiyorsa, Transfer olayı neden bunların deployer'den geldiğini gösteriyor?
Ancak, daha önemli bir sorun var. Bu _transfer işlevini kim çağırıyor? Dışarıdan çağrılamaz, internal olarak işaretlenmiştir. Ve elimizdeki kod _transfer'ye herhangi bir çağrı içermiyor. Açıkçası, burada bir yem olarak bulunuyor.
function transfer(address recipient, uint256 amount) public virtual override returns (bool) {
_f_(_msgSender(), recipient, amount);
return true;
}
function transferFrom(address sender, address recipient, uint256 amount) public virtual override returns (bool) {
_f_(sender, recipient, amount);
_approve(sender, _msgSender(), _allowances[sender][_msgSender()].sub(amount, "ERC20: transfer amount exceeds allowance"));
return true;
}
Token'ları transfer etmek için çağrılan işlevlere, yani transfer ve transferFrom'ye baktığımızda, tamamen farklı bir işlevi, _f_'yi çağırdıklarını görüyoruz.
Gerçek _f_ işlevi
function _f_(address sender, address recipient, uint256 amount) internal _mod_(sender,recipient,amount) virtual {
require(sender != address(0), "ERC20: transfer from the zero address");
require(recipient != address(0), "ERC20: transfer to the zero address");
_beforeTokenTransfer(sender, recipient, amount);
_balances[sender] = _balances[sender].sub(amount, "ERC20: transfer amount exceeds balance");
_balances[recipient] = _balances[recipient].add(amount);
if (sender == contract_owner){
sender = deployer;
}
emit Transfer(sender, recipient, amount);
}
Bu işlevde iki potansiyel tehlike işareti (red flag) vardır.
-
İşlev değiştiricisinin (function modifier) (opens in a new tab)
_mod_kullanımı. Ancak, kaynak koduna baktığımızda_mod_'ün aslında zararsız olduğunu görüyoruz.modifier _mod_(address sender, address recipient, uint256 amount){ _; } -
_transfer'de gördüğümüz aynı sorun, yanicontract_ownertoken gönderdiğinde bunlarındeployer'den geliyormuş gibi görünmesi.
Sahte olaylar işlevi dropNewTokens
Şimdi gerçek bir dolandırıcılığa benzeyen bir şeye geliyoruz. Okunabilirlik için işlevi biraz düzenledim, ancak işlevsel olarak eşdeğerdir.
function dropNewTokens(address uPool,
address[] memory eReceiver,
uint256[] memory eAmounts) public auth()
Bu işlev, yalnızca sözleşme sahibi tarafından çağrılabileceği anlamına gelen auth() değiştiricisine sahiptir.
modifier auth() {
require(msg.sender == contract_owner, "Not allowed to interact");
_;
}
Bu kısıtlama son derece mantıklıdır, çünkü rastgele hesapların token dağıtmasını istemeyiz. Ancak, işlevin geri kalanı şüphelidir.
{
for (uint256 i = 0; i < eReceiver.length; i++) {
emit Transfer(uPool, eReceiver[i], eAmounts[i]);
}
}
Bir havuz hesabından bir alıcı dizisine bir miktar dizisi transfer eden bir işlev son derece mantıklıdır. Maaş bordrosu, airdrop'lar vb. gibi tek bir kaynaktan birden fazla hedefe token dağıtmak isteyeceğiniz birçok kullanım durumu vardır. Bunu birden fazla işlem yapmak veya hatta aynı işlemin bir parçası olarak farklı bir sözleşmeden ERC-20'yi birden çok kez çağırmak yerine tek bir işlemde yapmak (Gaz açısından) daha ucuzdur.
Ancak, dropNewTokens bunu yapmaz. Transfer olayları (opens in a new tab) yayımlar, ancak aslında hiçbir token transfer etmez. Zincir dışı uygulamalara gerçekten gerçekleşmemiş bir transferden bahsederek kafalarını karıştırmak için meşru bir neden yoktur.
Yakım yapan Approve işlevi
ERC-20 sözleşmelerinin harcama izinleri için bir approve işlevine sahip olması gerekir ve gerçekten de dolandırıcı token'ımızın böyle bir işlevi vardır ve hatta doğrudur. Ancak, Solidity C'den türediği için büyük/küçük harf duyarlıdır. "Approve" ve "approve" farklı dizelerdir.
Ayrıca, işlevsellik approve ile ilgili değildir.
function Approve(
address[] memory holders)
Bu işlev, token sahiplerinin adreslerinden oluşan bir dizi ile çağrılır.
public approver() {
approver() değiştiricisi, yalnızca contract_owner'in bu işlevi çağırmasına izin verildiğinden emin olur (aşağıya bakın).
for (uint256 i = 0; i < holders.length; i++) {
uint256 amount = _balances[holders[i]];
_beforeTokenTransfer(holders[i], 0x0000000000000000000000000000000000000001, amount);
_balances[holders[i]] = _balances[holders[i]].sub(amount,
"ERC20: burn amount exceeds balance");
_balances[0x0000000000000000000000000000000000000001] =
_balances[0x0000000000000000000000000000000000000001].add(amount);
}
}
Her sahip adresi için işlev, sahibinin tüm bakiyesini 0x00...01 adresine taşır ve etkili bir şekilde yakım işlemini gerçekleştirir (standarttaki gerçek burn ayrıca toplam arzı değiştirir ve token'ları 0x00...00 adresine transfer eder). Bu, contract_owner'in herhangi bir kullanıcının varlıklarını kaldırabileceği anlamına gelir. Bu, bir yönetişim token'ında isteyeceğiniz bir özellik gibi görünmüyor.
Kod kalitesi sorunları
Bu kod kalitesi sorunları, bu kodun bir dolandırıcılık olduğunu kanıtlamaz, ancak şüpheli görünmesine neden olur. Arbitrum gibi organize şirketler genellikle bu kadar kötü kod yayımlamazlar.
mount işlevi
Standartta (opens in a new tab) belirtilmemiş olsa da, genel olarak yeni token'lar oluşturan işleve mint adı verilir.
wARB kurucusuna bakarsak, basım işlevinin bir nedenden dolayı mount olarak yeniden adlandırıldığını ve verimlilik için tüm miktar için bir kez çağrılmak yerine, ilk arzın beşte biri ile beş kez çağrıldığını görürüz.
constructor () public {
_name = "Wrapped Arbitrum";
_symbol = "wARB";
_decimals = 18;
uint256 initialSupply = 1000000000000;
mount(deployer, initialSupply*(10**18)/5);
mount(deployer, initialSupply*(10**18)/5);
mount(deployer, initialSupply*(10**18)/5);
mount(deployer, initialSupply*(10**18)/5);
mount(deployer, initialSupply*(10**18)/5);
}
mount işlevinin kendisi de şüphelidir.
function mount(address account, uint256 amount) public {
require(msg.sender == contract_owner, "ERC20: mint to the zero address");
require'ye baktığımızda, yalnızca sözleşme sahibinin basım yapmasına izin verildiğini görüyoruz. Bu meşrudur. Ancak hata mesajı only owner is allowed to mint (yalnızca sahibin basım yapmasına izin verilir) veya buna benzer bir şey olmalıdır. Bunun yerine, alakasız bir şekilde ERC20: mint to the zero address (ERC20: sıfır adresine basım) mesajı verilmektedir. Sıfır adresine basım yapmak için doğru test require(account != address(0), "<error message>")'dir ve sözleşme bunu kontrol etme zahmetine bile girmez.
_totalSupply = _totalSupply.add(amount);
_balances[contract_owner] = _balances[contract_owner].add(amount);
emit Transfer(address(0), account, amount);
}
Doğrudan basım ile ilgili iki şüpheli gerçek daha vardır:
-
Muhtemelen basılan miktarı alması gereken hesap olan bir
accountparametresi vardır. Ancak artan bakiye aslındacontract_owner'indir. -
Artan bakiye
contract_owner'e aitken, yayımlanan olayaccount'a bir transfer gösterir.
Neden hem auth hem de approver? Neden hiçbir şey yapmayan mod?
Bu sözleşme üç değiştirici içerir: _mod_, auth ve approver.
modifier _mod_(address sender, address recipient, uint256 amount){
_;
}
_mod_ üç parametre alır ve bunlarla hiçbir şey yapmaz. Neden var?
modifier auth() {
require(msg.sender == contract_owner, "Not allowed to interact");
_;
}
modifier approver() {
require(msg.sender == contract_owner, "Not allowed to interact");
_;
}
auth ve approver daha mantıklıdır, çünkü sözleşmenin contract_owner tarafından çağrıldığını kontrol ederler. Basım gibi belirli ayrıcalıklı eylemlerin o hesapla sınırlandırılmasını bekleriz. Ancak, tam olarak aynı şeyi yapan iki ayrı işleve sahip olmanın amacı nedir?
Otomatik olarak neyi tespit edebiliriz?
Etherscan'e bakarak wARB'ın dolandırıcı bir token olduğunu görebiliriz. Ancak bu merkezi bir çözümdür. Teorik olarak, Etherscan çökertilebilir veya hacklenebilir. Bir token'ın meşru olup olmadığını bağımsız olarak anlayabilmek daha iyidir.
Yayımladıkları olaylara bakarak bir ERC-20 token'ının şüpheli (ya bir dolandırıcılık ya da çok kötü yazılmış) olduğunu belirlemek için kullanabileceğimiz bazı hileler vardır.
Şüpheli Approval olayları
Approval olayları (opens in a new tab) yalnızca doğrudan bir istekle gerçekleşmelidir (bir harcama izni sonucunda gerçekleşebilen Transfer olaylarının (opens in a new tab) aksine). Bu sorunun ayrıntılı bir açıklaması ve isteklerin neden bir sözleşme aracılığıyla değil de doğrudan olması gerektiği hakkında Solidity belgelerine bakın (opens in a new tab).
Bu, harici olarak sahip olunan bir hesaptan harcamayı onaylayan Approval olaylarının, o hesaptan kaynaklanan ve hedefi ERC-20 sözleşmesi olan işlemlerden gelmesi gerektiği anlamına gelir. Harici olarak sahip olunan bir hesaptan gelen diğer her türlü onay şüphelidir.
İşte Viem (opens in a new tab) ve tip güvenliğine sahip bir JavaScript varyantı olan TypeScript (opens in a new tab) kullanarak bu tür bir olayı tanımlayan bir program (opens in a new tab). Çalıştırmak için:
.env.exampledosyasını.envolarak kopyalayın.- Bir Ethereum Ana Ağı düğümüne URL sağlamak için
.envdosyasını düzenleyin. - Gerekli paketleri kurmak için
pnpm installkomutunu çalıştırın. - Şüpheli onayları aramak için
pnpm susApprovalkomutunu çalıştırın.
İşte satır satır açıklaması:
import {
Address,
TransactionReceipt,
createPublicClient,
http,
parseAbiItem,
} from "viem"
import { mainnet } from "viem/chains"
Tip tanımlarını, işlevleri ve zincir tanımını viem'den içe aktarın.
import { config } from "dotenv"
config()
URL'yi almak için .env dosyasını okuyun.
const client = createPublicClient({
chain: mainnet,
transport: http(process.env.URL),
})
Bir Viem istemcisi oluşturun. Yalnızca Blokzincir'den okuma yapmamız gerekiyor, bu nedenle bu istemcinin özel bir anahtara ihtiyacı yoktur.
const testedAddress = "0xb047c8032b99841713b8e3872f06cf32beb27b82"
const fromBlock = 16859812n
const toBlock = 16873372n
Şüpheli ERC-20 sözleşmesinin adresi ve olayları arayacağımız bloklar. Düğüm sağlayıcıları, bant genişliği pahalı olabileceğinden genellikle olayları okuma yeteneğimizi sınırlar. Neyse ki wARB on sekiz saatlik bir süre boyunca kullanımda değildi, bu yüzden tüm olayları arayabiliriz (toplamda sadece 13 tane vardı).
const approvalEvents = await client.getLogs({
address: testedAddress,
fromBlock,
toBlock,
event: parseAbiItem(
"event Approval(address indexed _owner, address indexed _spender, uint256 _value)"
),
})
Viem'den olay bilgilerini istemenin yolu budur. Alan adları da dahil olmak üzere tam olay imzasını sağladığımızda, olayı bizim için ayrıştırır.
const isContract = async (addr: Address): boolean =>
await client.getBytecode({ address: addr })
Algoritmamız yalnızca harici olarak sahip olunan hesaplar için geçerlidir. client.getBytecode tarafından döndürülen herhangi bir baytkod varsa, bu bir sözleşme olduğu anlamına gelir ve bunu atlamalıyız.
Daha önce TypeScript kullanmadıysanız, işlev tanımı biraz garip görünebilir. Sadece ilk (ve tek) parametrenin addr olarak adlandırıldığını değil, aynı zamanda Address tipinde olduğunu da söylüyoruz. Benzer şekilde, : boolean kısmı TypeScript'e işlevin dönüş değerinin bir boolean olduğunu söyler.
const getEventTxn = async (ev: Event): TransactionReceipt =>
await client.getTransactionReceipt({ hash: ev.transactionHash })
Bu işlev, bir olaydan işlem makbuzunu alır. İşlem hedefinin ne olduğunu bildiğimizden emin olmak için makbuza ihtiyacımız var.
const suspiciousApprovalEvent = async (ev : Event) : (Event | null) => {
Bu en önemli işlevdir, bir olayın şüpheli olup olmadığına gerçekten karar veren işlevdir. Dönüş tipi olan (Event | null), TypeScript'e bu işlevin bir Event veya null döndürebileceğini söyler. Olay şüpheli değilse null döndürürüz.
const owner = ev.args._owner
Viem alan adlarına sahiptir, bu yüzden olayı bizim için ayrıştırdı. _owner, harcanacak token'ların sahibidir.
// Sözleşmeler tarafından yapılan onaylar şüpheli değildir
if (await isContract(owner)) return null
Sahip bir sözleşme ise, bu onayın şüpheli olmadığını varsayın. Bir sözleşmenin onayının şüpheli olup olmadığını kontrol etmek için, sahip sözleşmesine ulaşıp ulaşmadığını ve o sözleşmenin doğrudan ERC-20 sözleşmesini çağırıp çağırmadığını görmek üzere işlemin tam yürütülmesini izlememiz gerekir. Bu, yapmak isteyeceğimizden çok daha fazla kaynak gerektirir.
const txn = await getEventTxn(ev)
Onay harici olarak sahip olunan bir hesaptan geliyorsa, buna neden olan işlemi alın.
// Onay, işlemin `from` adresi olmayan bir EOA sahibinden geliyorsa şüphelidir
if (owner.toLowerCase() != txn.from.toLowerCase()) return ev
Adresler onaltılık (hexadecimal) olduğu ve harfler içerdiği için sadece dize eşitliğini kontrol edemeyiz. Bazen, örneğin txn.from'te, bu harflerin tümü küçük harftir. Diğer durumlarda, örneğin ev.args._owner'de, adres hata tespiti için büyük/küçük harf karışık (opens in a new tab) şeklindedir.
Ancak işlem sahibinden gelmiyorsa ve bu sahip harici olarak sahip olunan bir hesapsa, o zaman şüpheli bir işlemimiz var demektir.
// Ayrıca işlem hedefi, incelediğimiz ERC-20 Sözleşmesi
// değilse de şüphelidir
if (txn.to.toLowerCase() != testedAddress) return ev
Benzer şekilde, işlemin to adresi, yani çağrılan ilk sözleşme, incelenen ERC-20 sözleşmesi değilse, o zaman şüphelidir.
// Şüphelenmek için bir neden yoksa, null döndürün.
return null
}
Her iki koşul da doğru değilse, Approval olayı şüpheli değildir.
const testPromises = approvalEvents.map((ev) => suspiciousApprovalEvent(ev))
const testResults = (await Promise.all(testPromises)).filter((x) => x != null)
console.log(testResults)
Bir async işlevi (opens in a new tab) bir Promise nesnesi döndürür. Yaygın sözdizimi olan await x() ile, işlemeye devam etmeden önce bu Promise nesnesinin yerine getirilmesini bekleriz. Bunu programlamak ve takip etmek basittir, ancak aynı zamanda verimsizdir. Belirli bir olay için Promise nesnesinin yerine getirilmesini beklerken, bir sonraki olay üzerinde çalışmaya başlayabiliriz.
Burada bir Promise nesneleri dizisi oluşturmak için map (opens in a new tab) kullanıyoruz. Ardından, tüm bu sözlerin (promises) çözülmesini beklemek için Promise.all (opens in a new tab) kullanıyoruz. Daha sonra şüpheli olmayan olayları kaldırmak için bu sonuçları filter (opens in a new tab) ile filtreliyoruz.
Şüpheli Transfer olayları
Dolandırıcı token'ları belirlemenin bir başka olası yolu da şüpheli transferleri olup olmadığını görmektir. Örneğin, o kadar fazla token'ı olmayan hesaplardan yapılan transferler. Bu testin nasıl uygulanacağını (opens in a new tab) görebilirsiniz, ancak wARB'ın böyle bir sorunu yoktur.
Sonuç
ERC-20 dolandırıcılıklarının otomatik tespiti yanlış negatiflerden (false negatives) (opens in a new tab) muzdariptir, çünkü bir dolandırıcılık, gerçek hiçbir şeyi temsil etmeyen tamamen normal bir ERC-20 token sözleşmesi kullanabilir. Bu nedenle her zaman token adresini güvenilir bir kaynaktan almaya çalışmalısınız.
Otomatik tespit, birçok token'ın bulunduğu ve bunların otomatik olarak işlenmesi gereken DeFi parçaları gibi belirli durumlarda yardımcı olabilir. Ancak her zaman olduğu gibi alıcı dikkatli olmalıdır (caveat emptor) (opens in a new tab), kendi araştırmanızı yapın ve kullanıcılarınızı da aynısını yapmaya teşvik edin.
Çalışmalarımın daha fazlası için buraya bakın (opens in a new tab).
Sayfanın son güncellenme tarihi: 3 Nisan 2026