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'ı (yeni sekmede açılır) 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 tespit edebileceğimizi görüyoruz.
Dolandırıcı token'lar - nedir, insanlar neden 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 kullanıcı perspektifinden daha fazla bilgiyi ethereum.org'un başka bir yerinde okuyabilirsiniz. Bu eğitim, 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 (yeni sekmede açılır) eşdeğermiş gibi davranan wARB (yeni sekmede açılır)'dır.
Hangisinin meşru token olduğunu anlamanın en kolay yolu, onu oluşturan organizasyona, yani Arbitrum (yeni sekmede açılır)'a bakmaktır. Meşru adresler kendi belgelerinde (yeni sekmede açılır) 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 (yeni sekmede açılır) ve bu (yeni sekmede açılır)).
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 (yeni sekmede açılır), 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çebilse de, yanlış kaynak kodunu yayımlayamazlar. Blok gezgini, sağlanan kaynak kodunu bağımsız olarak derler ve tam olarak aynı baytkodu elde edemezse, bu kaynak kodunu reddeder. Bu konu hakkında daha fazla bilgiyi Etherscan sitesinde okuyabilirsiniz (yeni sekmede açılır).
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 (yeni sekmede açılır), Ownable (yeni sekmede açılır) modelini kullanır. Ayrıcalıklı adres, depolamada _owner adlı bir alanda tutulur (üçüncü dosyaya, Ownable.sol dosyasına bakın).
abstract contract Ownable is Context {
address private _owner;
.
.
.
}
ARB token sözleşmesinin (yeni sekmede açılır) doğrudan ayrıcalıklı bir adresi yoktur. Ancak, buna ihtiyacı da yoktur. 0xb50721bcf8d664c30412cfbc6cf7a15145234ad1 adresindeki (yeni sekmede açılır) bir proxy (yeni sekmede açılır) 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 dosyasına bakın).
/**
* @dev EIP1967 yönetici 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ü bir contract_owner adresi vardır.
contract WrappedArbitrum is Context, IERC20 {
.
.
.
address deployer = 0xB50721BCf8d664c30412Cfbc6cf7a15145234ad1;
address public contract_owner = 0xb40dE7b1beE84Ff2dc22B70a049A07A13a411A33;
.
.
.
}
Bu sözleşme sahibi (yeni sekmede açılır), farklı zamanlarda farklı hesaplar tarafından kontrol edilebilecek bir sözleşme değil, dışarıdan sahipli bir hesaptır. Bu, değerli kalacak bir ERC-20'yi kontrol etmek için uzun vadeli bir çözüm olmaktan ziyade, muhtemelen 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 (yeni sekmede açılır) son işleme (yeni sekmede açılır) 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 adresinden 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 işlevine herhangi bir çağrı içermiyor. Açıkçası, burada bir tuzak 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 transfer ve transferFrom işlevlerine baktığımızda, tamamen farklı bir işlevi, _f_ işlevini ç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 vardır.
-
İşlev değiştiricisinin (modifier) (yeni sekmede açılır)
_mod_kullanımı. Ancak, kaynak koduna baktığımızda_mod_değiştiricisinin aslında zararsız olduğunu görüyoruz.modifier _mod_(address sender, address recipient, uint256 amount){ _; } -
_transferiçinde gördüğümüz aynı sorun, yanicontract_ownertoken gönderdiğinde bunlarındeployeradresinden 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 token'ları tek bir kaynaktan birden fazla hedefe 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ı (yeni sekmede açılır) yayımlar, ancak aslında hiçbir token transfer etmez. Zincir dışı uygulamalara gerçekten gerçekleşmemiş bir transferi bildirerek onların kafası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 böyle bir işleve sahiptir 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 adresinin 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, sahibin 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 adresinin 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 (yeni sekmede açılır) belirtilmemiş olsa da, genel olarak yeni token'lar oluşturan işleve mint adı verilir.
wARB kurucu işlevine 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 ifadesine 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ı verilmiştir. Sıfır adresine basım yapmak için doğru test require(account != address(0), "<error message>") şeklindedir 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_owneradresine aittir. -
Artan bakiye
contract_owneradresine aitken, yayımlanan olayaccountadresine bir transfer gösterir.
Neden hem auth hem de approver var? Neden hiçbir şey yapmayan mod var?
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 bu 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 neleri tespit edebiliriz?
Etherscan'e bakarak wARB sözleşmesinin 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ı (yeni sekmede açılır) yalnızca doğrudan bir istekle gerçekleşmelidir (bir harcama izni sonucunda gerçekleşebilen Transfer olaylarının (yeni sekmede açılır) 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 (yeni sekmede açılır).
Bu, dışarıdan sahipli 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. Dışarıdan sahipli bir hesaptan gelen diğer her türlü onay şüphelidir.
İşte tip güvenliğine sahip bir JavaScript varyantı olan TypeScript (yeni sekmede açılır) ve Viem (yeni sekmede açılır) kullanarak bu tür bir olayı tanımlayan bir program (yeni sekmede açılır). Ç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 yüklemek 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"
Tür tanımlarını, işlevleri ve zincir tanımını viem içinden 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 nedenle 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 dışarıdan sahipli 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 atlamamız gerekir.
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 türünde olduğunu da söylüyoruz. Benzer şekilde, : boolean kısmı TypeScript'e işlevin dönüş değerinin bir boolean (mantıksal değer) 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üş türü olan (Event | null), TypeScript'e bu işlevin 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 verilen 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, işlemin tam yürütülmesini izleyerek sahip sözleşmesine ulaşıp ulaşmadığını ve bu sözleşmenin doğrudan ERC-20 sözleşmesini çağırıp çağırmadığını görmemiz gerekir. Bu, yapmak isteyeceğimizden çok daha fazla kaynak gerektirir.
const txn = await getEventTxn(ev)
Onay dışarıdan sahipli bir hesaptan geliyorsa, buna neden olan işlemi alın.
// Onay, işlemin `from` değeri 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 içinde, bu harflerin tümü küçük harftir. Diğer durumlarda, örneğin ev.args._owner gibi, adres hata tespiti için büyük/küçük harf karışık (yeni sekmede açılır) şeklindedir.
Ancak işlem sahibinden gelmiyorsa ve bu sahip dışarıdan sahipli bir hesapsa, o zaman şüpheli bir işlemimiz var demektir.
// İşlem hedefi, incelemekte olduğumuz 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, bu şü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 (yeni sekmede açılır) 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 (yeni sekmede açılır) kullanıyoruz. Ardından, tüm bu sözlerin (promises) çözülmesini beklemek için Promise.all (yeni sekmede açılır) kullanıyoruz. Daha sonra şüpheli olmayan olayları kaldırmak için bu sonuçları filter (yeni sekmede açılır) 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ı (yeni sekmede açılır) görebilirsiniz, ancak wARB bu soruna sahip değildir.
Sonuç
ERC-20 dolandırıcılıklarının otomatik tespiti yanlış negatiflerden (yeni sekmede açılır) 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) (yeni sekmede açılır), 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 (yeni sekmede açılır).