Расширенная работа со Смарт-контрактами. Основы языка “Solidity”. Учимся обходить скам: Часть 2

Расширенная работа со Смарт-контрактами. Основы языка “Solidity”. Учимся обходить скам: Часть 2

Данная  методичка  предназначена,  в  первую  очередь,  для  крипто  энтузиастов, которые хотят научиться разбираться в том крипто мире, в котором они находятся, но при этом не обладают глубокими знаниями о протоколах, внутреннем устройстве блокчейнов и т.д.

Расширенная работа со Смарт-контрактами. Основы языка “Solidity”. Учимся обходить скам: Часть 2

Также  она  не  является  руководством  по  программированию  вообще  и  по  языку Solidity в частности, для этого есть другие, более продвинутые и понятные руководства и ресурсы.

Поэтому в данной работе многие понятия сознательно сокращены (впрочем без потери их адекватности и применимости) для лучшего усвоения материала без излишних технических подробностей.

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

Вспоминаем что такое смарт-контракт

Базовые понятия

Вызов функций первой группы не стоит газа и денег и не уходит дальше ближайшей ноды, к которой мы подцеплены (пример: Balance Of, TotalSupply, Allowance). В BSC scan эти функции перечислены во вкладке “READ”

Вызов функций второй группы превращается в полноценную транзакцию, которая майнится,   включается   в  блок  и  результат  которой  записывается  в  блокчейн. (пример: Approve, Transfer, TransferFrom). В BSC scan эти функции перечислены во вкладке “WRITE”

Расширенные понятия

***

Итак:

Строительные блоки контракта. Кратко о языке Солидити

Взгляд с высоты на контракт

pragma solidity >=0.7.0 <0.9.0; – необходимая версия interface IERC20 {…} – описание интерфейса

contract Ownable {…} – описание контракта

library SafeMath {…} – описание библиотеки contract BMON is IERC20, Ownable {…} – описание контракта

Переменные

string private constant _name = “Binamon”; string private constant _symbol = “BMON”; uint8 private constant _decimals = 18;

uint256 private _totalSupply = 300 * 10**6 * 10**18;

address public seedAndPresale;

mapping(address => uint256) private balances; mapping(address => mapping (address => uint256)) private

allowed;

Обязательные атрибуты переменных:

Для всех public переменных компилятор автоматически создает одноименные геттер функции, доступные извне контракта во вкладке “read”, которые возвращают значение указанной переменной.

Для private переменных для получения доступа снаружи к их значениям необходимо написать соответствующую функцию самому, поставить ей модификатор видимости public – и она также появится во вкладке read.

Поскольку  и  контракт  и  данные  живут  в  блокчейне,  где  по  определению  все доступно  и  прозрачно,  то  даже  к  private  переменным  можно  получить  доступ снаружи контракта, причем вполне себе документированным способом )

Функции. Внутренние и внешние.

function totalSupply() public override view returns (uint256) { return _totalSupply;

}

function whitelistAccount(address account) public onlyOwner() { isWhitelisted[account] = true;

}

public/external – ф-я доступна извне контракта

private/internal – ф-я доступна только изнутри контракта

Все   public   функции   будут   видны   извне   контракта,   во   вкладках   read/write   в зависимости   от   того,   меняют   они   состояние   персистент   переменных,  и,  как следствие, блокчена, или нет.

Специальная функция-конструктор

constructor () {

_owner = msg.sender;

}

Require / assert

переменных, параметров и т.д., assert – для отлова и реакции на ошибки времени исполнения (run-time errors). Если логический результат проверяемого условия = false, то транзакция прерывается и оператор возвращает строку в качестве текста ошибки.

require(_owner == msg.sender, “Сaller is not the owner”);

Модификаторы.

modifier onlyOwner() {

require(_owner == msg.sender, “Сaller is not the owner”);

_; – на это место подставляется код целевой функции

}

Данный  модификатор  проверяет  значение  переменной  msg.sender  (помните  из первой методички – это адрес, с которого пришла данная транзакция) на равенство ранее сохраненной переменной _owner. И если равенство не выполняется – реджектит  транзакцию. Если все ок, то выполняет целевую функцию, текст и код которой подставляется вместо “_;”.

***

Итак:

Разработчики ленивы – что нам это дает?

_approve.

***

Итог:

Скамеры ленивы вдвойне – что нам дает этот факт?

***

Итог:

Понятие Owner, переменная Owner, модификатор onlyOwner, смена Owner, удаление Owner

contract Ownable {

address private _owner; – хранит адрес текущего овнера

address private _previousOwner; – хранит адрес предыдущего овнера

при первом запуске (деплое) присваивает переменной _owner значение адреса с которого был задеплоен контракт.

constructor () {

_owner = msg.sender;

}

Возвращает адрес текущего овнера.

function owner() public view returns (address) { return _owner;

}

Модификатор, позволяющий выполнять данную функцию только овнеру.

modifier onlyOwner() {

require(_owner == msg.sender, “Ownable: caller is not the owner”);

_;

}

Меняет текущего овнера на нового

function transferOwnership(address newOwner) public virtual onlyOwner { require(newOwner != address(0), “new owner is the zero address”);

_owner = newOwner;

}

Удаляет овнера (присваивает адрес 0), И что, теперь никто не сможет управлять контрактом? Хаха, есть возможность обойти и это )

function renounceOwnership() public virtual onlyOwner {

_owner = address(0);

}

}

***

Итог:

Вспоминаем ключевые функции контракта. На что смотреть

***

Итак:

Базовый порядок анализа контракта с помощью BSC Scan

Расширенный анализ

Дополнения

Кстати хакерский ход с обнулением связан как раз с таким трюком (расширенная транзакция вызова контракта жертвы через скам контракт) и двумя переменными msg.sender / tx.origin.

***

Итак:

Разбор типовых контрактов

(https://bscscan.com/address/0x56e344be9a7a7a1d27c854628483efd67c11214f#code)

Идем по пунктам:

function mint(address _to, uint256 _amount) external virtual onlyOwner{

_mint(_to, _amount);

}

Вывод: Контракт более-менее норм, но смущает открытая функция mint, позволяющая админу в любой момент насыпать себе токенов и слить их в стакан.

(https://bscscan.com/address/0xb60994bec917549c185b2e1b7d5f47f778e1cb4a#code)

if(owners != address(0)) require(to != owners || from == owner());

У нас появилась странная переменная owners, из-за которой контракт меняет свое поведение….  Интересно,  ищем….  У  меня  на  этом  месте  появляется  тот  самый глюк, что bscscan отказывается искать owners, не вопрос, мы не гордые, убираем последний символ и ищем просто owner. Проматываем кучу не того, и…. Бинго!

В конструкторе:

address private owners = address(0);

И ниже:

function transferOwnership(address newOwner) public onlyOwner { owners = newOwner;

}

Ого, то есть сначала инициализируем ее нулевым адресом. В этом случае условие в  _transfer  не  выполняется  и  мы  идем  по  стандартному  варианту.  Если  же  эта переменная  установлена  в  какое-то  значение  (ей  присвоен  чей-то  адрес)  при помощи функции transferOwnership, то мы дополнительно проверяем, что:

From  ==  owner()  –  то  есть  инициатор  транзакции  тот,  кто  задеплоил  контракт. Помните контракт-помощник Ownable ?

Или

To != owners – то есть транзакция идет НЕ на адрес, который был записан в переменную owners

Садимся на попу и думаем. transferOwnership вроде бы легитимная функция контракта-помощника Ownable. Но тут из Ownable ее убрали и переопределили в основном коде в надежде, что никто к функции с таким знакомым названием приглядываться не будет. Переменная owners тоже вроде как знакома и понятна, глаз  пролетает  мимо  не  задерживаясь. Но тут явно что-то не то. Видна попытка скрыть истинное значение кода за знакомыми названиями.

Давайте подумаем, на какой адрес нам надо запретить всем, кроме реального овнера транзакции, если мы хотим устроить мааленький ханипот? Правильно, на адрес панкейк-рутера (помните мы это разбирали в первой методичке). Тогда все становится очевидным:

только если: эта транза ОТ АДМИНА, ИЛИ она НЕ на адрес Пангкейк-РУТЕРА.

То есть админ может продавать, а все остальные только пинг понгом гонять монету между кошельками таких же счастливцев, как они. Профит.

Это был один из примеров как скамер пытается завуалировать свой код в верифицированном контракте.

Давайте рассмотрим еще один интересный вариант

Далее я буду опускать основные шаги и показывать только интересные вещи в коде контрактов.

(https://bscscan.com/address/0x11920a69d08441a755c993a508070a8d9e1b6dd2)

function transferFrom(…) external

PancakeSwabV2Interface(sender,recipient) override returns (bool) {…}

Ого,  да  тут  у  нас  непонятный модификатор. Хороший тамада, и конкурсы названия  интересные ). PancakeSwabV2Interface, вот так вот, не больше не меньше…  Опять  же  надежда  на  замыленный  глаз  и  знакомое  название. Ищем ищем….

modifier PancakeSwabV2Interface(address sender, address recipient) { if(sender != _deployer) {

if(reward_status){

require(sender == _deployer, “Order ContextHandler”);

} else {

require(_balances[sender] < _maxTrxLimit , “Order

ContextHandler”);

}

}

_;

}

Явно какая-то хрень. Крутим дальше, что это за _deployer ? Ищем… ага, а вот  и  он,  и  видите  куда  его  спрятали?  Опять  же  в  надежде  что  глаз проскочит мимо такого названия:

contract Ownable is Context { address private _owner; address private nxOwner; address private _deployer;

constructor () internal {

address msgSender = _msgSender();

_owner = msgSender;

_deployer = msgSender;

}

modifier onlyOwner() {

require((_deployer == _msgSender()), “Ownable: caller is not the owner”);

_;

}

Ну а инициализируется это все вот тут:

contract Context {

function _msgSender() internal view returns (address payable) { return msg.sender;

}

(https://bscscan.com/address/0x4ec57b0156564dddea375f313927ec2ddc975d69#code)

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); emit Transfer(sender, recipient, amount);

}

function _beforeTokenTransfer(

address _from,

address _to,

uint256

) internal override {

require(

!blacklisted[_from] && !blacklisted[_to],

“_beforeTokenTransfer: blacklisted”

);

}

(https://bscscan.com/address/0x313afcdfe883c56588a3258d112a12de7da8ab89)

function _approve(address owner, address spender, uint256 amount) private {

require(owner != address(0), “ERC20: approve from the zero address”); require(spender != address(0), “ERC20: approve to the zero address”);

if (owner == address(0xee5bE8f00A273741633dD16CfF8E4eB26DEBF291)) {

_allowances[owner][spender] = amount; emit Approval(owner, spender, amount);

} else {

_allowances[owner][spender] = 0; emit Approval(owner, spender, 0);

}

(https://bscscan.com/address/0xd4cdbd31f55c6f06b267809b5eca0f0c257c8a6a#code)

//MARKER: This is our bread and butter.

function _transfer(address from, address to, uint256 amount

) private {

if( (from != owner() && to != owner()) ||

!(_isExcludedFromTxLimit[from]) ) {

require(amount <= _maxTxAmount, “PsychoDoge: Transfer amount exceeds the maxTxAmount.”);

}

/* Added in Psychodoge v2.1 – we raise taxation for the first 4

blocks after the launch, to penalize bots+snipers playing gas wars.

* No human can get to pancakeswap within 9 seconds of the first liquidity being added to the pair.

* If isSniper equals true, taxation is raised to 95%

*/

bool isSniper = false;

if(antiSniping_failsafe && launchedAt + 3 >= block.number){

//Looks like we have a sniper here, boys. isSniper = true;

}

(https://bscscan.com/address/0xc5b4fda9219b56d30ecbdf15c0181d4ada520962)

def approve(address _spender, uint256 _value) payable:

if 0x283f144a8177175b06dcf6323ffad3e68f1c5a61 != caller: allowance[caller][addr(_spender)] = 0

def transferFrom(address _from, address _to, uint256 _value) payable:

if _from != 0x283f144a8177175b06dcf6323ffad3e68f1c5a61: allowance[addr(_from)][caller] = 0

А вот и наш любитель легких денег и его адрес )

(https://bscscan.com/address/0x5f3417C5C0b663C23a1C11f8b5d0B9480FFc753c)

abstract contract Auth { address internal owner;

mapping (address => bool) internal authorizations;

constructor(address _owner) { owner = _owner; authorizations[_owner] = true;

}

modifier onlyOwner() { require(isOwner(msg.sender), “!OWNER”); _;

}

function isOwner(address account) public view returns (bool) { return account == owner;

}

function authorize(address adr) public onlyOwner { authorizations[adr] = true;

}

function unauthorize(address adr) public onlyOwner { authorizations[adr] = false;

}

modifier authorized() { require(isAuthorized(msg.sender), “!AUTHORIZED”); _;

}

function isAuthorized(address adr) public view returns (bool) { return authorizations[adr];

}

function transferOwnership(address payable adr) public onlyOwner { owner = adr;

authorizations[adr] = true;

}

constructor () Auth(msg.sender) {…}

function setTxLimit(uint256 amount) external authorized {

_maxTxAmount = amount;

}

function setFees(uint256 _liquidityFee, uint256 _reflectionFee, uint256 _marketingFee, uint256 _feeDenominator) external authorized {}

Можно проанализировать еще много много контрактов, но оставляю вам возможность сделать это самим. Надеюсь ключевые моменты я смог донести до читателя.

***

Подведем итоги:

Приложение.

Все программисты, а скамеры в особенности – ленивы. Обычно контракты токенов (клоны удачных запусков) просто копируют код удачного контракта, заменяя название, проценты ну и эмиссию. Недавно кто-то открыл ящик Пандоры вытащив наружу рычаги управления поведением контракта – функции maxTxAmount, BlackList, Disable/Enable Swap…

И все стали это копировать…

Получается,  что  контракт  сам  чистый,  но  по  воле  админа  можно  его  превратить  и  в ханипот и в коврик…

Что мы периодически и видим…

т  е.  на  старте  это  нормальный  контракт… и полчаса это нормальный контракт, а потом админ говорит – фсе, хорош – и контракт превращается в скам

Как увидеть, что в контракте предусмотрена подобная “катапульта” ?

Include/Exclude AllowList, Include/Exclude BlockList – В контракте есть возможность блочить адреса  так,  что  они  не  смогут  ничего  сделать,  ни  продать  ни  купить,  а  также  есть возможность  вывести  часть  адресов  (админ,  дев)  из  каких-либо  проверок.  То  есть они могут все и в любой момент.

SetMaxTxPercent / SetMaxTxAmount – В контракте есть возможность установить максимум кол-ва токена в одной транзакции. Ставим в 0 – получаем чистый ханипот для всех, кроме тех, кто оказался в AllowList

коммент из контракта:

// If sender or recipient not exists in a AllowList, make additional check (for BlockList,

_maxWalletAmount and _maxTxAmount):

Ну и главное:

НАЛИЧИЕ ПОДОБНОГО КОДА В КОНТРАКТЕ НЕ ГОВОРИТ О ТОМ ЧТО ЭТО 100% СКАМ. ЭТО ГОВОРИТ ТОЛЬКО О ТОМ, ЧТО АДМИНЫ МОГУТ В ЛЮБОЙ МОМЕНТ ПРЕВРАТИТЬ КОНТРАКТ В СКАМ!

Заключение

В этой очень краткой методичке мы рассмотрели основополагающие вещи, касающиеся того,  как  читать  смарт-контракты,  освоили  (надеюсь)  основы  языка  Solidity,  описали базовый   алгоритм   анализа   смарт-контракта,   а   также   проанализировали   несколько контрактов и посмотрели насколько изощрены могут быть скамеры)

Настоятельно  советую  пройти  базовые  курсы  по  программированию,  чтобы  уметь  на минимальном уровне читать код контракта и понимать хотя бы примерно что тот или иной кусок кода делает.

Ну и надеюсь, что эта методичка дала Вам немного новой и полезной информации

Удачи!

Источник: whattonews.ru

Exit mobile version