Аналіз смарт-контрактів
27.03.2024

Приховані пастки в Honeypot. Як розпізнати приманку й уникнути втрати активів?

Смарт-контракти Honeypot - це один із видів шахрайських смарт-контрактів, які виглядають привабливо для користувачів і обіцяють фінансову вигоду. Однак такі контракти містять приховані пастки або шкідливий код, внаслідок чого користувачі, навпаки, втрачають свої кошти.

Щоб захистити себе від смарт-контрактів Honeypot необхідно ретельно вивчати і перевіряти код контракту перед початком роботи з ним. Світ смарт-контрактів і блокчейна постійно розвивається, регулярно з'являються нові вразливості та способи обману, тому розробникам, аудиторам і користувачам вкрай важливо бути в курсі останніх тенденцій і розробок у цій галузі.

Існує безліч прикладів смарт-контрактів Honeypot. У цій статті ми розглянемо деякі з прикладів.

1. Fallback function honeypot

У цьому типі honeypot контракт має вразливу функцію "відкату", яка, здавалося б, дозволяє будь-якій людині стати власником контракту і зняти його баланс. Однак така функція містить приховану умову, що перешкоджає успішному виконанню.

Приклад коду:


pragma solidity 0.4.18

contract FallbackHoneypot {
    address public owner;

    function() public payable {
        if (owner == 0) {
            owner = msg.sender;
        } else {
            revert();
        }
    }

    function withdraw() public {
        require(msg.sender == owner);
        msg.sender.transfer(this.balance);
    }
}

У цьому прикладі функція fallback дає змогу будь-якому користувачеві стати власником. Однак оператор revert() гарантує, що зміна власника ніколи не відбудеться, залишаючи кошти зловмисника в пастці контракту.

2. Hidden condition honeypot

Такі контракти містять приховану умову або вимогу, яку потрібно бути виконано для успішного виконання удаваної вразливої функції. Користувачі можуть вважати, що можуть використовувати контракт, але прихована умова гарантує, що вони не зможуть цього зробити.

Приклад коду:


pragma solidity 0.4.18

contract HiddenConditionHoneypot {
    uint256 public counter = 0;

    function deposit() public payable {
        require(msg.value == 1 ether);
        counter++;
    }

    function withdraw() public {
        require(counter > 100);
        msg.sender.transfer(this.balance);
    }
}

У цьому контракті користувач може внести 1 ефір, і, здавалося б, після 100 депозитів він може зняти весь баланс контракту. Однак умова counter > 100 гарантує, що виведення коштів можливе тільки після 101 депозиту, що затримує кошти в контракті.

3. Reentrancy honeypot

У цьому типі honeypot контракт виявляється вразливим для атаки reentrancy, коли зловмисник може неодноразово викликати функцію контракту під час під час виведення коштів. Однак контракт містить прихований механізм, який запобігає успішній експлуатації.

Приклад коду:


pragma solidity 0.4.18

contract ReentrancyHoneypot {
    mapping(address => uint256) public balances;

    function deposit() public payable {
        balances[msg.sender] += msg.value;
    }

    function withdraw(uint256 _amount) public {
        require(balances[msg.sender] >= _amount);
        if (msg.sender.call.value(_amount)()) {
            balances[msg.sender] -= _amount;
        }
    }
}

У цьому прикладі контракт виявляється вразливим для атаки reentrancy через використання msg.sender.call.value(_amount)(). Однак контракт не дозволяє користувачам поповнювати рахунок на суму, що перевищує початковий баланс, що фактично затримує їхні кошти.

reentrancy honeypot

4. Gas limit honeypot

У цьому типі honeypot контракт начебто дозволяє користувачеві вивести кошти, але кількість gas, необхідна для виконання функції виведення, перевищує ліміт gas-блока, що робить транзакцію неможливою.

Приклад коду:


pragma solidity 0.4.18

contract GasLimitHoneypot {
    uint256 constant public numArrayElements = 1000;
    uint256[numArrayElements] public someArray;

    function deposit() public payable {
        for (uint256 i = 0; i < numArrayElements; i++) {
            someArray[i] = 0;
        }
    }

    function withdraw() public {
        uint256 balance = address(this).balance;
        for (uint256 i = 0; i < numArrayElements; i++) {
            someArray[i] = 1;
        }
        msg.sender.transfer(balance);
    }
}

У цьому прикладі функція deposit ініціалізує масив із 1000 елементами. Функція withdraw переводить баланс контракту відправнику, але при цьому змінює елементи масиву. Gas, необхідний для виконання циклу, перевищує ліміт gas-блоку, що не дає змоги здійснити виведення коштів і затримує їх у контракті.

5. Timestamp manipulation honeypot

У цьому типі honeypot контракт використовує тимчасову мітку блокчейна Ethereum як умову виконання. Однак, оскільки майнери можуть маніпулювати тимчасовою міткою блоку, контракт стає вразливим.

Приклад коду:


pragma solidity 0.4.18

contract TimestampHoneypot {
    uint256 public lastInteraction;
    uint256 public prize;

    function deposit() public payable {
        lastInteraction = now;
        prize += msg.value;
    }

    function withdraw() public {
        require(now >= lastInteraction + 1 hours);
        msg.sender.transfer(prize);
        prize = 0;
    }
}

У цьому контракті користувачі можуть вносити кошти і знімати весь баланс контракту після закінчення 1 години з моменту останньої взаємодії. Однак, оскільки майнери можуть маніпулювати тимчасовою міткою блоку, вони можуть запустити функцію зняття раніше, ніж очікувалося, і тим самим фактично вкрасти кошти.

6. Hidden transfer honeypot

У цьому типі honeypot контракт містить прихований переказ коштів на адресу зловмисника, що створює видимість легітимного контракту, але в дійсності призводить до втрати коштів.

Приклад коду:


pragma solidity 0.4.18

contract HiddenTransferHoneypot {
    address public owner;

    constructor() public {
        owner = msg.sender;
    }

    function deposit() public payable {
        if (msg.value >= 1 ether) {
            owner.transfer(msg.value);
        }
    }

    function withdraw() public {
        require(msg.sender == owner);
        owner.transfer(address(this).balance);
    }
}

У цьому прикладі контракт начебто дозволяє вносити і знімати кошти. Однак функція поповнення рахунку містить прихований переказ коштів власнику (зловмиснику), якщо сума поповнення дорівнює або більша за 1 ефіру. Нічого не підозрюючи користувачі, які поповнили рахунок, втратять свої ефіри, оскільки вони будуть переведені на адресу зловмисника.

7. Reentrancy honeypot

Reentrancy honeypot заснований на використанні уразливості reentrancy в контракті, коли функція контракту може бути викликана рекурсивно до оновлення його стану.

Приклад коду:


pragma solidity 0.4.18

contract ReentrancyHoneypot {
    mapping(address => uint256) public balances;

    function deposit() public payable {
        balances[msg.sender] += msg.value;
    }

    function withdraw() public {
        uint256 amount = balances[msg.sender];
        (bool success,) = msg.sender.call.value(amount)("");
        require(success, "Withdrawal failed.");
        balances[msg.sender] = 0;
    }
}

У цьому прикладі контракт дозволяє введення і виведення коштів. Однак він вразливий для reentrancy-атак. Зловмисник може створити шкідливий контракт, який буде рекурсивно викликати функцію withdraw до того, як баланс буде встановлено в нуль, що призведе до виснаження всього балансу контракту.

8. Type casting and overflow honeypot

У цьому типі honeypot зловмисник використовує приведення типів і цілочисельні переповнення, щоб обдурити користувачів і змусити їх повірити, що контракт безпечний, але насправді він містить приховані пастки.

Приклад коду:


pragma solidity 0.4.18

contract TypeCastingHoneypot {
    uint8 public count = 0;
    uint256 public reward = 1 ether;

    function increment() public payable {
        require(msg.value >= reward);

        uint8 prevCount = count;
        count++;

        if (count < prevCount) {
            msg.sender.transfer(address(this).balance);
        }
    }
}

У цьому прикладі контракт винагороджує користувачів, які збільшують змінну count під час надсилання суми, що дорівнює або перевищує розмір винагороди. Однак через використання uint8 для змінної count при досягненні значення 255 відбувається цілочисельне переповнення. Після переповнення лічильник скидається в 0, і зловмисник може отримати весь баланс контракту.

overflow honeypot

9. Delegatecall honeypot

У цьому типі honeypot зловмисник використовує функцію delegatecall для виконання шкідливого коду від імені, здавалося б, безпечного контракту.

Приклад коду:


pragma solidity 0.4.18

contract DelegateCallHoneypot {
    address public owner;

    constructor() public {
        owner = msg.sender;
    }

    function() external payable {}

    function withdraw() public {
        require(msg.sender == owner);
        owner.transfer(address(this).balance);
    }

    function execute(address _target, bytes memory _data) public {
        require(msg.sender == owner);
        (bool success,) = _target.delegatecall(_data);
        require(success, "Execution failed.");
    }
}

У цьому прикладі контракт дозволяє поповнення, зняття коштів і виконання коду через функцію execute. Однак зловмисник може створити шкідливий код, який під час виконання функції delegatecall зможе маніпулювати станом контракту, зокрема змінною owner. Змінивши власника, зловмисник може отримати контроль над контрактом і спустошити його баланс.

10. Hidden storage manipulation honeypot

У цьому типі honeypot зловмисник приховано маніпулює змінними сховища, щоб обдурити користувачів і отримати контроль над контрактом.

Приклад коду:


pragma solidity 0.4.18

contract HiddenStorageManipulation {
    mapping(address => uint256) public balances;
    uint256 public totalSupply;
    address public owner;

    constructor() public {
        owner = msg.sender;
        balances[owner] = 1000;
        totalSupply = 1000;
    }

    function transfer(address _to, uint256 _value) public {
        require(balances[msg.sender] >= _value);
        balances[msg.sender] -= _value;
        balances[_to] += _value;

        if (totalSupply - balances[owner] >= 1000) {
            owner = _to;
        }
    }
}

У цьому прикладі контракт виглядає як простий токен-контракт. Користувачі можуть передавати токени один одному, а власником спочатку встановлено творця контракту. Однак зловмисник може приховано маніпулювати змінними totalSupply і balances, щоб повернути контроль над контрактом. Коли різниця між totalSupply і balances[owner] досягає 1000, змінна owner оновлюється, що дозволяє зловмиснику повернути контроль над контрактом.

11. Function name collision honeypot

У цьому типі honeypot зловмисник використовує колізії імен функцій, щоб обманом змусити користувачів викликати неправильну функцію.

Приклад коду:


pragma solidity 0.4.18

contract FunctionNameCollision {
    address public owner;

    constructor() public {
        owner = msg.sender;
    }

    function withdraw() external {
        require(msg.sender == owner);
        msg.sender.transfer(address(this).balance);
    }

    function withdraw(uint256 amount) external {
        require(msg.sender == owner);
        owner.transfer(amount);
    }

    function() external payable {}
}

У цьому прикладі контракт має дві функції зняття коштів. Одна з них дозволяє власнику зняти весь залишок коштів, а інша - певну суму. Однак якщо користувач спробує зняти певну суму, викликавши функцію withdraw(uint256), то через колізію імен функцій він мимоволі викличе функцію withdraw() без аргументів, що призведе до зняття всього балансу контракту.

12. Delegatecall vulnerability honeypot

У цьому типі honeypot зловмисник використовує delegatecall для виконання шкідливої функції в іншому контракті, що може призвести до крадіжки коштів у користувачів.

Приклад коду:


pragma solidity 0.4.18

contract DelegatecallVulnerability {
    address public owner;

    constructor() public {
        owner = msg.sender;
    }

    function withdraw() external {
        require(msg.sender == owner);
        msg.sender.transfer(address(this).balance);
    }

    function () external payable {
        if (msg.value > 1 ether) {
            this.delegatecall(msg.data);
        }
    }
}

contract Malicious {
    function withdraw() external {
        msg.sender.transfer(address(this).balance);
    }
}                

У цьому прикладі контракт здається нешкідливим, з простою функцією withdraw(), що дозволяє власнику зняти баланс контракту. Однак у контракті також присутня функція fallback, яка при отриманні більше 1 ефіру виконує delegatecall іншого контракту, використовуючи msg.data. Якщо зловмисник створить шкідливий контракт із функцією withdraw(), яка викрадає баланс контракту, і відправить більше 1 ефіру на контракт-жертву, то delegatecall виконає шкідливу функцію withdraw(), викравши баланс контракту.

vulnerability honeypot

13. Pseudo-integers overflow honeypot

Цей тип honeypot заснований на тому, що Solidity не має вбудованої підтримки чисел з плаваючою точкою. Зловмисники можуть створювати контракти, які зовні використовують десяткові числа, але насправді маніпулюють цілими числами обманним шляхом.

Приклад коду:


pragma solidity 0.4.18

contract PseudoIntegersOverflow {
    mapping(address => uint256) public balances;

    function deposit() external payable {
        require(msg.value > 0);
        uint256 pseudoValue = msg.value * 100;
        balances[msg.sender] += pseudoValue;
    }

    function withdraw(uint256 amount) external {
        uint256 pseudoAmount = amount * 100;
        require(balances[msg.sender] >= pseudoAmount);
        balances[msg.sender] -= pseudoAmount;
        msg.sender.transfer(amount);
    }
}

У цьому прикладі створюється враження, що контракт управляє балансом користувача з десятковою точністю, множачи значення на 100 перед їхнім збереженням. Однак такий підхід вводить в оману, оскільки Solidity не підтримує числа з плаваючою точкою. Коли користувач намагається зняти свої кошти, контракт невірно обчислює псевдосуму через цілочисельне усічення, і користувач може виявитися не в змозі зняти весь свій баланс.

Для захисту від такого типу honeypot необхідно розуміти обмеження мови Solidity і те, як вона обробляє числові операції. Будьте обережні під час роботи з контрактами, у яких заявлено підтримку десяткової точності, і завжди перевіряйте код контракту на наявність потенційних проблем.

14. Hidden fees in smart contracts

Деякі шкідливі смарт-контракти можуть нав'язувати користувачам приховані платежі без їхнього відома. Такі платежі можуть бути замасковані в коді контракту або спрацьовувати тільки за певних умов.

Приклад коду:


pragma solidity 0.4.18

contract HiddenFees {
    address public owner;
    uint256 public feePercentage;

    constructor(uint256 _feePercentage) public {
        owner = msg.sender;
        feePercentage = _feePercentage;
    }

    function withdraw(uint256 amount) external {
        uint256 fee = amount * feePercentage / 100;
        uint256 netAmount = amount - fee;
        require(address(this).balance >= netAmount);
        msg.sender.transfer(netAmount);
        owner.transfer(fee);
    }
}

У цьому прикладі творець контракту під час розгортання встановлює розмір плати (feePercentage). Під час взаємодії з контрактом користувачі можуть не знати про цю комісію. Коли користувач знімає свої кошти, контракт розраховує комісію, віднімає її від суми зняття і відправляє частину, що залишилася, користувачеві. Потім комісія перераховується власнику контракту.

Щоб не потрапити в цю пастку, уважно вивчіть код контракту і переконайтеся, що вам зрозумілі всі пов'язані з ним комісії. Шукайте приховані або завуальовані розрахунки.

15. Hidden state manipulation

У деяких випадках шкідливі контракти можуть дозволити власнику контракту або зловмиснику маніпулювати внутрішнім станом контракту, що призведе до несподіваних наслідків для інших користувачів.

Приклад коду:


pragma solidity 0.4.18

contract HiddenStateManipulation {
    address public owner;
    mapping(address => uint256) public balances;

    constructor() public {
        owner = msg.sender;
    }

    function deposit() external payable {
        balances[msg.sender] += msg.value;
    }

    function withdraw(uint256 amount) external {
        require(balances[msg.sender] >= amount);
        msg.sender.transfer(amount);
        balances[msg.sender] -= amount;
    }

    function manipulateBalance(address user, uint256 newBalance) external {
        require(msg.sender == owner);
        balances[user] = newBalance;
    }
}

У цьому прикладі контракт дає змогу користувачам вносити і знімати кошти. Однак функція manipulateBalance дозволяє власнику контракту довільно змінювати баланс будь-якого користувача. Це може призвести до несподіваних втрат для користувачів або дозволити власнику контракту вкрасти кошти.

Необхідно уважно вивчати код контракту на предмет наявності функцій, що дають змогу маніпулювати станом, особливо якщо доступ до них має тільки власник контракту або певні адреси.

hidden state manipulation

16. Hidden token stealing

У деяких випадках шкідливі контракти можуть містити приховані функції, що дають змогу власнику контракту або зловмиснику викрадати токени у користувачів, які нічого не підозрюють.

Приклад коду:


pragma solidity 0.4.18

contract HiddenTokenStealing {
    address public owner;
    mapping(address => uint256) public balances;

    constructor() public {
        owner = msg.sender;
    }

    function deposit(IERC20 token, uint256 amount) external {
        require(token.transferFrom(msg.sender, address(this), amount));
        balances[msg.sender] += amount;
    }

    function withdraw(IERC20 token, uint256 amount) external {
        require(balances[msg.sender] >= amount);
        require(token.transfer(msg.sender, amount));
        balances[msg.sender] -= amount;
    }

    function stealTokens(IERC20 token, address user, uint256 amount) external {
        require(msg.sender == owner);
        require(token.transfer(owner, amount));
        balances[user] -= amount;
    }
}

У цьому прикладі контракт дозволяє користувачам вводити і виводити токени ERC20. Однак функція stealTokens дозволяє власнику контракту вкрасти токени у будь-якого користувача.

Висновок:

Honeypot - це тільки одна з безлічі шахрайських схем у DeFi, яка використовує не тільки обмеження на виведення коштів, а й різні приховані пастки, описані вище.

Щоб не потрапити на приманку, необхідно розуміти особливості шахрайської схеми, тонкощі мови програмування Solidity і ретельно вивчати смарт-контракти перед взаємодією з ними.

Залишаючись поінформованим і проводячи ретельну перевірку, ви зможете мінімізувати ризики, пов'язані з використанням смарт-контрактів.

 

Якщо у вас з'явилися сумніви в безпеці або функціональності смарт-контракту скористайтеся нашою платформою Lotus Market.

 

Платформа Lotus Market створена командою досвідчених розробників і професійних аудиторів. Мета проєкту - мінімізувати ризики інвестування в шахрайські токени та допомогти безпечно і комфортно торгувати криптовалютами.

З повагою, команда Lotus Market.

All posts

Connect to a wallet

Metamask