Solidity. Смарт контракты и аудит – Telegram
Solidity. Смарт контракты и аудит
2.62K subscribers
246 photos
7 videos
18 files
547 links
Обучение Solidity. Уроки, аудит, разбор кода и популярных сервисов
Download Telegram
Front-running атаки. Часть 2

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

Контракт NFTRefinanceMarket в приведенном ниже примере предназначен для работы в качестве торговой площадки или депозитария, где владельцы NFT могут депонировать свои NFT, возможно, в качестве залога для получения кредита или участия в другой децентрализованной финансовой деятельности (DeFi). Предполагаемый рабочий процесс использует стандартный механизм утверждения ERC-721 и требует от владельца NFT двух отдельных транзакций:

1. Транзакция approve: владелец NFT сначала вызывает функцию approve() в самом контракте NFT, предоставляя адресу контракта NFTRefinanceMarket разрешение на управление конкретным tokenId.

2. Транзакция рефинансирования: затем владелец NFT вызывает функцию refinance() в контракте NFTRefinanceMarket, указав тот же tokenId. Эта функция предназначена для:

- Проверки наличия необходимого аппрува из шага 1.
- Передачу NFT от владельца в хранение контракту NFTRefinanceMarket.
- Запись адреса, который вызвал refinance() (предположительно, первоначальный владелец), в отображении tokenCreditors, связывая его с депонированным NFT.

По сути, это безопасный двухэтапный процесс, позволяющий владельцу зафиксировать свой NFT в маркет контракте и отслеживать свои права собственности/кредиторские права с помощью него. Вот как это выглядит:

pragma solidity ^0.8.0;


import "forge-std/Test.sol";
import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/access/Ownable.sol";


interface INFT is IERC721 {
function mint(address to, uint256 tokenId) external;
}


contract NFTRefinanceMarket is Ownable {
INFT public nft;
mapping(uint256 => address) public tokenCreditors;


constructor(INFT _nft) Ownable(msg.sender) {
nft = _nft;
}


function refinance(uint256 _tokenId) external {
// Check if this contract has approval to transfer the NFT
require(nft.getApproved(_tokenId) == address(this), "Not approved");
address originalOwner = nft.ownerOf(_tokenId);


// Pull the NFT into this contract as collateral
nft.transferFrom(originalOwner, address(this), _tokenId);


// Record the caller as the creditor for this NFT
tokenCreditors[_tokenId] = msg.sender;
}
}


Однако злоумышленник может опередить транзакцию, фактически похитив NFT, как показано ниже:

function testFrontRunRefinance() public {
// 1. User approves the contract to refinance their NFT
assertEq(nft.ownerOf(tokenId), user);
vm.startPrank(user);
nft.approve(address(nftRefinanceMarket), tokenId);
assertEq(nft.getApproved(tokenId), address(nftRefinanceMarket));
vm.stopPrank();


// 2. Attacker monitors the mempool, and before the user calls refinance, the attacker calls refinance
vm.startPrank(attacker);


// Simulate the attacker front-running the transaction
// In a real scenario, the attacker would increase the gas price to get their transaction included first
nftRefinanceMarket.refinance(tokenId);


// Verify that the attacker is now marked as the creditor for this NFT
assertEq(nftRefinanceMarket.tokenCreditors(tokenId), attacker);


// Verify that the NFT now belongs to the contract
assertEq(nft.ownerOf(tokenId), address(nftRefinanceMarket));
vm.stopPrank();


// 3. User tries to refinance the NFT, but it has already been refinanced by the attacker
vm.startPrank(user);
vm.expectRevert("Not approved");
nftRefinanceMarket.refinance(tokenId);
vm.stopPrank();
}
1
Этот тип атаки происходит из-за разрыва между двумя транзакциями, когда злоумышленник может вмешаться между этими вызовами.

Как же этого избежать?

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

#frontrun
👍10
Front-running атаки. Часть 3

Могут ли пользователи злонамеренно вызвать отмену транзакций других пользователей, опередив их с помощью «пыли»?

Атаки с использованием «пыли» предполагают, что злоумышленники опережают законные транзакции с помощью крошечных сумм токенов, что может привести к сбою исходной транзакции. Изменяя состояние цепочки, транзакция злоумышленника делает исполнение транзакции жертвы недействительными, что в конечном итоге приводит к ее сбою. Это похоже на то, как слегка сдвинуть кусочек пазла, чтобы помешать кому-то собрать картинку.

Вот упрощенный сценарий:

// Contract with a function that requires zero balance
contract Auction {
IERC20 public token;
event AuctionStarted(uint256 id);


constructor(address _token) {
token = IERC20(_token);
}


// Function that requires zero balance to execute
function startAuction() external returns (uint256) {
// Vulnerable check that can be exploited
require(token.balanceOf(address(this)) == 0, "Balance must be zero");


// Start auction logic would go here
uint256 id = 1;
emit AuctionStarted(id);
return id;
}
}


Уязвимость в этом контракте аукциона заключается в том, что злоумышленник может помешать кому-либо начать аукцион с помощью атаки «пыль».

Если Алиса захочет начать аукцион и отправит транзакцию с вызовом startAuction(), злоумышленник Боб может опередить ее транзакцию, отправив минимальное количество токенов (пыль) на адрес контракта. Когда транзакция Алисы будет обработана, проверка require(token.balanceOf(address(this)) == 0, «Balance must be zero») завершится сбоем, поскольку баланс контракта больше не будет равен нулю, что приведет к отмене ее транзакции.

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

#frontrun
👍8
Front-running атаки. Часть 4

Использует ли протокол правильно привязанную к пользователю схему "commit-reveal"?

Схемы "commit-reveal" (фиксация-раскрытие) предназначены для защиты конфиденциальных действий в блокчейне, таких как ставки на аукционе, от утечки информации и упреждающих сделок (фронт-раннинга) в процессе подтверждения транзакции. В типичной схеме пользователи сначала отправляют в контракт commitment (криптографический хэш от планируемого действия и секретной "соли"). После окончания установленного периода фиксации начинается период раскрытия, в течение которого пользователи отправляют свое исходное действие и соль. Контракт проверяет, что хэш от переданных действия и соли совпадает с ранее сохраненным commitment'ом. Это похоже на то, как вы запечатываете свою ставку в конверте, подаете его и вскрываете только после того, как все ставки поданы.

Однако схема может быть уязвима, если сам commitment не привязан уникальным образом к пользователю, который его отправил. Плохо спроектированная схема может позволить злоумышленнику, который наблюдает за транзакцией commitment'а пользователя (например, в мемпуле), скопировать этот commitment или вмешаться в процесс раскрытия. Данная конкретная уязвимость фокусируется на том, как отсутствие идентификатора пользователя внутри хэша commitment'а позволяет атакующему сорвать аукцион. Это как если бы кто-то идеально скопировал содержимое вашего запечатанного конверта и подал его точную копию от своего имени.

Давайте рассмотрим пример:

pragma solidity ^0.8.0;


import "forge-std/Test.sol";


/**
* Overview:
* Checklist Item ID: SOL-AM-FrA-4
*
* This test demonstrates a front-running vulnerability in a commit-reveal auction contract.
* The vulnerability is that the protocol doesn't include the committer's address in the commitment,
* allowing anyone to reveal another user's commitment and claim the reward.
*/


contract Auction {
mapping(address => bytes32) public commitments;
address public winner;
uint256 public winningBid;
bool public revealed;
uint256 public endTime;


constructor(uint256 _duration) {
endTime = block.timestamp + _duration;
}


// Users commit their bids with a salt
function commit(bytes32 commitment) public {
require(block.timestamp < endTime, "Auction ended");
commitments[msg.sender] = commitment;
}


// Vulnerable reveal function - doesn't include committer address in the commitment
function reveal(uint256 bid, bytes32 salt) public {
require(block.timestamp > endTime, "Reveal time not reached");
require(!revealed, "Already revealed");


// Vulnerability: commitment doesn't include the committer's address
// This allows anyone to create the same commitment with the same bid and salt
bytes32 expectedCommitment = keccak256(abi.encode(bid, salt));


// The attacker can commit the same value and then reveal it
require(commitments[msg.sender] == expectedCommitment, "Invalid commitment");


// The revealer becomes the winner
if (bid > winningBid) {
winningBid = bid;
winner = msg.sender;
}
revealed = true;
}


function claimReward() public view returns (address) {
require(block.timestamp > endTime && revealed, "Auction not ended or not revealed");
return winner;
}
}


Суть уязвимости, продемонстрированной кодом:

- Хэш commitment'а (keccak256(abi.encode(bid, salt))) вычисляется без включения адреса участника (msg.sender).

- Поскольку адрес участника не является частью хэша, любой, кто узнает ставку (bid) и соль (salt), использованные честным участником, может вычислить точно такой же хэш commitment'а.

- Атакующий наблюдает за транзакцией честного участника и вызывает commit() с идентичным хэшем. Теперь и адрес честного участника, и адрес атакующего связаны с одним и тем же значением bytes32 в маппинге commitments.
1🔥1
- Функция reveal() проверяет commitments[msg.sender] == expectedCommitment. Это означает, что она проверяет, зафиксировал ли ранее вызывающий reveal (то есть msg.sender в этом вызове) хэш, соответствующий предоставленным bid и salt.

contract AuctionTest is Test {
Auction public auction;
address public bidder1;
address public bidder2;
uint256 public auctionDuration = 1 days;


function setUp() public {
bidder1 = address(1);
bidder2 = address(2);
auction = new Auction(auctionDuration);
vm.warp(block.timestamp + 1 minutes);
}


function testFrontRunningReveal() public {
uint256 bid1 = 1 ether;
bytes32 salt1 = keccak256("secret1");


// Both bidders create the same commitment with the same bid and salt
// This is possible because the commitment doesn't include the committer's address
bytes32 commitment = keccak256(abi.encode(bid1, salt1));


// Bidder1 commits first
vm.prank(bidder1);
auction.commit(commitment);


// Attacker sees the bid and salt values (e.g., from mempool or other side channel)
// and commits the same values
vm.prank(bidder2);
auction.commit(commitment);


vm.warp(block.timestamp + auctionDuration);


// Attacker front-runs by revealing first
vm.prank(bidder2);
auction.reveal(bid1, salt1);


// Attacker becomes the winner by front-running
assertEq(auction.winner(), bidder2);
assertEq(auction.winningBid(), bid1);
}
}


Поскольку атакующий действительно зафиксировал идентичный хэш, он может успешно вызвать reveal(bid, salt). Если он сделает это раньше честного участника (например, заплатив более высокую комиссию за газ, чтобы его транзакция была обработана первой — front-run), он пройдет проверку commitments[атакующий] == expectedCommitment. Адрес атакующего (msg.sender внутри этого вызова reveal) затем будет установлен как победитель.

Как обеспечить уникальную привязку commitment'а к участнику?

- Включите адрес участника (committer) в данные, которые хэшируются при создании commitment'а (обычно это делается пользователем off-chain). Commitment должен вычисляться так: commitment = keccak256(abi.encode(msg.sender, bid, salt)) (могут быть и другие возможные дополнительные поля, например, chainId, nonce и т.д., которые можно добавить в зависимости от случая использования).

- Функция reveal затем должна воссоздать хэш, используя ту же структуру, включая msg.sender (тот, кто вызывает reveal): bytes32 expectedCommitment = keccak256(abi.encode(msg.sender, bid, salt));

#frontrun
👍7
Griefing атаки. Часть 1

Давайте разбираться с очередным видом атак.

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

Термин «грифинг» (griefing), вероятно, происходит из сообществ онлайн-игр, где он описывает игроков, которые намеренно раздражают и донимают других, часто нарушая предполагаемый ход игры ради собственного развлечения, а не стратегического преимущества. Точно так же в контексте смарт контрактов грифинг атаки ставят в основу создание неудобств, а не извлечение прибыли.

Стоит отметить, что в сфере безопасности web3 термины «грифинг» и «отказ в обслуживании (DoS)» иногда используются как взаимозаменяемые, что может вызывать путаницу. Однако понимание их различий может прояснить цель каждой из атак.

Атака типа «отказ в обслуживании» (DoS) — это термин, укоренившийся в общей безопасности сетей и компьютеров. Ее цель — сделать сервис или сеть недоступными для всех пользователей на время или на неопределенный срок. Это может включать в себя перегрузку системы трафиком или эксплуатацию уязвимостей, которые предотвращают законный доступ для всех. Целью, как правило, является широкомасштабный срыв работы всего сервиса.

Масштаб и намерение являются основными различиями между этими двумя типами атак. Однако грань иногда может быть размытой, а это означает, что некоторые уязвимости могут относиться к любой из категорий. Крупная грифинг атака потенциально может привести к сценарию DoS для части пользователей или функций.

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

Существует ли внешняя функция, которая зависит от состояний, которые могут быть изменены другими?

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

Меры по устранению: Убедитесь, что обычные действия пользователей, особенно важные, такие как withdrawal (вывод средств) и repayment (погашение), не могут быть нарушены другими участниками.

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

Если контракт позволяет любому изменять переменную состояния, от которой зависит выполнение критической операции другого пользователя (например, условие вывода, флаг или временная метка), злоумышленник может злонамеренно изменить это состояние, чтобы заблокировать жертву. Злоумышленник специально нацеливается на способность жертвы исполнять последующие транзакции, часто тратя на это собственный газ. то особенно практично в блокчейнах с низкой комиссией за транзакции.

Рассмотрим контракт VulnerableVault с отсроченным выводом.


// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;


contract VulnerableVault {
uint256 public delay = 60 minutes;
mapping(address => uint256) public lastDeposit;
mapping(address => uint256) public balances;


function deposit(address _for) public payable {
lastDeposit[_for] = block.timestamp;
balances[_for] += msg.value;
}


function withdraw(uint256 _amount) public {
require(block.timestamp >= lastDeposit[msg.sender] + delay,
"Wait period not over");
require(balances[msg.sender] >= _amount, "Insufficient funds");
balances[msg.sender] -= _amount;
(bool success,) = payable(msg.sender).call{value: _amount}("");
require(success, "Transfer failed");
}
}


Функция deposit(address _for) является слабым местом. Она позволяет вызывающему (msg.sender) указать любой адрес _for. При вызове этой функции обновляется lastDeposit[_for]. Злоумышленник может вызвать эту функцию, указав адрес своей жертвы в качестве _for, и отправить минимальную сумму (например, 1 вей). Это действие сбрасывает временную метку lastDeposit жертвы.
3
Поскольку функция withdraw проверяет lastDeposit[msg.sender], действие злоумышленника напрямую мешает жертве (когда она является msg.sender в вызове withdraw) удовлетворить условию по времени. Злоумышленник усугубляет положение жертвы, сбрасывая ее таймер вывода средств.

Исправление: запретить пользователям изменять критические переменные состояния, принадлежащие другим пользователям. Самым простым способом исправления здесь является ограничение функции deposit только обновлением состояния вызывающего (msg.sender).

// Corrected deposit function (inside VulnerableVault)
function deposit() public payable {
// Only affects the caller's state
lastDeposit[msg.sender] = block.timestamp;
balances[msg.sender] += msg.value;
}


Теперь только Алиса может обновлять lastDeposit[alice].

#griefing
👍6
Griefing атаки. Часть 2

Могут ли операции контракта быть манипулированы с помощью точного указания лимита газа?

Злоумышленники могут указывать тщательно рассчитанные объемы газа, чтобы принудительно направить выполнение контракта по определенному пути, тем самым манипулируя его поведением неожиданным образом.

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

Ярким примером такой манипуляции является "груминг" (намеренное причинение вреда) из-за недостаточного газа при внешних вызовах. Хотя в рекомендациях по устранению упоминаются «явные проверки газа», которые часто означают require(gasleft() > MIN_GAS_NEEDED), данная конкретная атака, связанная с внешними вызовами, наиболее эффективно предотвращается иным способом.

В этом сценарии злоумышленник использует контракт, который вызывает внешний контракт, но не проверяет, успешно ли завершился этот внешний вызов. Злоумышленник формирует транзакцию, указывая ровно столько газа, чтобы выполнить логику вызывающего контракта до момента внешнего вызова и, возможно, даже обновить внутреннее состояние преждевременно, но недостаточно газа для успешного завершения внешнего вызова в целевом контракте. Если вызывающий контракт не проверяет статус успеха, он может завершить свое выполнение, полагая, что все прошло успешно, оставляя систему в несогласованном состоянии, где внутренние записи не соответствуют реальному результату неудачного внешнего взаимодействия.

Продемонстрируем это на примере контракта Relayer:

1. Функция forward контракта Relayer предназначена для выполнения действия через внешний вызов контракта Target.

2. Ключевой момент: функция forward обновляет внутреннее состояние (помечает запрос _data как выполненный для защиты от повторного выполнения), прежде чем совершается внешний вызов или подтверждается его успешность.

3. Функция не проверяет статус успешности, возвращаемый вызовом target.call(...).

4. Злоумышленник вызывает forward, указывая тщательно рассчитанный лимит газа: достаточно для прохождения проверки require и обновления executed[_data] = true, но недостаточно для успешного завершения последующего вызова target.call(...) внутри контракта Target.

5. Вызов target.call исчерпывает выделенный газ и молча завершается с ошибкой (с точки зрения контракта Relayer, поскольку его успешность не проверяется). Однако функция forward завершается «успешно».

6. В результате возникает несогласованное состояние: отображение executed в контракте Relayer показывает, что действие выполнено, но необходимая внешняя операция в контракте Target фактически не произошла. Конкретная транзакция легитимного пользователя (_data) теперь навсегда заблокирована из-за механизма защиты от повторного выполнения, который по сути подвергся цензуре со стороны злоумышленника, воспользовавшегося механикой газа и отсутствием проверки ошибок.
👍3🔥1
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.17;


// Target contract with a function that consumes some gas
contract Target {
uint256 private _storedData = 0;
function execute(bytes memory _data) external {
// Simulate some work that consumes gas
uint256 i;
while(i < 100) {
i++;
_storedData += i;
}
}
}


// Relayer contract vulnerable to insufficient gas griefing
contract Relayer {
mapping (bytes => bool) public executed; // Replay protection mapping
address public target;


constructor(address _target) {
target = _target;
}


function forward(bytes memory _data) public {
// Check replay protection
require(!executed[_data], "Replay protection");
executed[_data] = true;


// Vulnerability: External call is made, but its success status is NOT checked.
// If target.call runs out of gas (due to limited gas sent by attacker),
// this function DOES NOT revert. The state change above persists.
target.call(abi.encodeWithSignature("execute(bytes)", _data));
}
}


Функция Relayer.forward помечает параметр _data как выполненный до вызова контракта Target и, что критически важно, не проверяет возвращаемое значение успешности вызова target.call. Злоумышленник использует эту уязвимость, отправляя транзакцию с ровно таким количеством газа, которого хватит для выполнения проверки require и строки executed[_data] = true, но недостаточно для завершения внутренней логики вызова target.call. Внешний вызов завершается неудачно из-за нехватки газа, однако транзакция в контракте Relayer продолжается и успешно завершается, поскольку ошибка не была обнаружена. В результате флаг executed устанавливается в true, портя состояние системы и делая невозможным успешную передачу реального _data — даже несмотря на то, что функция Target.execute в рамках атакующей транзакции так и не была полностью выполнена.

Как поправить: Хотя общая рекомендация предлагает «явные проверки газа», для конкретной уязвимости (манипуляция потоком выполнения через нехватку газа при внешнем вызове) наиболее прямое и надежное решение — проверять статус успешности внешнего вызова.

Низкоуровневый метод .call возвращает булево значение success (успех). Это значение обязательно должно проверяться. Если success равно false (что происходит, если внешний вызов откатывается или исчерпывает газ), вызывающая функция должна также откатиться — обычно с помощью require(success, "Сообщение об ошибке"). Это предотвращает успешное завершение транзакции в несогласованном состоянии.

Использование require(gasleft() > MIN_GAS_NEEDED) перед внешним вызовом, как правило, менее эффективно для решения именно этой проблемы, поскольку фактическое потребление газа внешним контрактом может быть непредсказуемым или намеренно завышено злонамеренным целевым контрактом. Проверка require(success) корректно обрабатывает результат независимо от причины сбоя.

// Исправленный фрагмент функции forward (внутри Relayer)

function forward(bytes memory _data) public {
require(!executed[_data], "Защита от повторного выполнения");
executed[_data] = true;

// Взаимодействие: выполнение внешнего вызова

(bool success, ) = target.call(abi.encodeWithSignature("execute(bytes)", _data));
require(success, "Внешний вызов завершился неудачно");

}


Эта исправленная логика гарантирует, что транзакция завершится и флаг executed будет установлен в true только в случае реального успешного выполнения target.call. Она напрямую предотвращает атаку манипуляции газом, откатывая всю транзакцию при неудаче внешнего вызова и тем самым сохраняя согласованность состояния.

#griefing
👍81👌1
Атака майнеров. Часть 1

Сегодня мы углубимся в тему атак майнеров. Валидаторы (ранее известные как майнеры в системах Proof of Work) — это процессоры транзакций, которые обладают значительным контролем над механикой блокчейна. Понимание их привилегий в рамках протоколов консенсуса имеет важное значение для реализации безопасных смарт контрактов.

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

В частности, мы рассмотрим следующие пункты контрольного списка:

SOL-AM-MA-1: Используется ли block.timestamp для операций, чувствительных ко времени?

SOL-AM-MA-2: Использует ли контракт свойства блока, такие как временная метка или сложность, для генерации случайных чисел?

SOL-AM-MA-3: Чувствительна ли логика контракта к порядку транзакций?

Давайте углубимся в тему и узнаем, как защитить ваши смарт контракты от этих тонких, но потенциально разрушительных атак!

Влияние майнеров на блокчейн системы

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

Эволюция Ethereum от PoW к Proof of Stake (PoS) превратила «майнеров» в «валидаторов». Тем не менее, сообщество специалистов по безопасности блокчейна продолжает использовать такие термины, как «извлекаемая майнерами ценность» - MEV - и «манипуляции майнеров» из-за их исторической распространенности и технической преемственности. Однако в настоящее время эти атаки осуществляются валидаторами, которые контролируют порядок транзакций в экосистеме Ethereum после слияния.

Майнеры обладают несколькими предоставленными протоколом возможностями, которые напрямую влияют на работу блокчейна. Они определяют включение транзакций и их порядок в блоках на основе стимулов в виде комиссий (цен на газ в Ethereum), что дает им контроль над очередностью обработки мемпула. Эта возможность определять порядок позволяет извлекать максимальную извлекаемую ценность (MEV) путем стратегического позиционирования транзакций без нарушения правил консенсуса. Майнеры могут манипулировать временными метками блоков в пределах допустимых сетью допусков, которые обычно составляют ±15 секунд в Ethereum и до 2 часов в Bitcoin, что может потенциально повлиять на логику контрактов, зависящих от времени, таких как периоды разблокировки или расчет процентов.

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

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

Существуют и другие интересные векторы атак, в том числе:

- Атаки 51%: контроль над большей частью хешрейта для обеспечения двойной траты;

- Эгоистичный майнинг: стратегическое удержание блоков для увеличения относительного вознаграждения;

- Timejacking: манипулирование восприятием времени в сети;

- Атаки Eclipse: изоляция узлов от честных пиров;
👍2🔥2🤔1
Однако для этого обычно требуются либо значительные вычислительные ресурсы, либо сложные механизмы управления сетью, выходящие за рамки стандартных операций майнинга. Такие атаки нацелены на уязвимости уровня консенсуса, а не на эксплуатацию контрактов на уровне приложений, и поэтому выходят за рамки наших непосредственных соображений по безопасности смарт контрактов.

Далее разберем несколько таких атак.

#miner #mev
👍6
Проект на виду. Часть 8. Какой контекст у смарт контракта?

Если вы уже некоторое время на канале, то, вероятно, помните, что я потихоньку разбираюсь и делаю свой небольшой проект на граница ИИ и Web3. По сути, это будет небольшой ассистент, который будет помогать аудировать код контракта и акцентировать внимание на похожих уязвимостях, которые были указаны в отчетах аудиторских компаний за продолжительный срок.

На данный момент я собрал около 28К уязвимостей из таких отчетов. Было бы больше 35К-40К, если бы я не удалял большинство Low и 99,5% Ingo/Gas описаний багов (честно говоря, во многих компаниях эти разделы откровенный мусор!).

Сейчас я в стадии ранжирования этих уязвимостей и определения "веса" каждой из них, для более аккуратных рекомендаций ассистента. Но сейчас не об этом.

На днях я столкнулся с проблемой, а что такое "контекст контракта"?

С одной стороны, "контекст контракта" - это его описание и основное назначение. Но насколько хорошим будет такой контекст для ответов на вопросы по нему?

С другой стороны, контракт - это набор определенных функций. И может стоит рассматривать контекст через призму его функций?

Или описание контекста функций недостаточно без детализации их внешних и внутренних действий? Кроме того, большую роль в контексте функций являются изменения состояний переменных хранилища и использование storage/memory пространств...

Кроме того, глупо рассматривать контекст отдельного контракта, когда он работает сообща с другими контрактами в протоколе и вне его.

А документация? Своя и протоколов, с которыми он работает... Это же тоже все должно быть частью контекста...

И вот получается, что для создания какого-то общего контекста проекта нужно как-то не понятным образом обработать огромные платы данных, и где есть вероятность, что они просто не поместятся в один запрос к ИИ на обработку.

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

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

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

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

С этим постом я просто хотел поделиться с вами, чем сейчас занимаюсь и куда стремлюсь. И если у вас появятся идеи или что-то по поводу контекста смарт контрактов, буду рад, если поделитесь в комментариях. Это очень поможет моей текущей работе.

#ai #tool
👍6🔥1
Атака майнеров. Часть 2

Используется ли block.timestamp для операций, чувствительных ко времени?

Майнеры могут манипулировать block.timestamp на несколько секунд, что может повлиять на логику контрактов, зависящих от времени.

Как предотвратить: используйте block.number вместо временных меток для критически важных операций, связанных со временем, или убедитесь, что допустимая погрешность манипуляций приемлема.

block.timestamp представляет время, которое майнер предлагает для блока. Хотя оно обычно точное, майнеры имеют некоторую свободу для его корректировки. Эта свобода допускает потенциальные уязвимости в логике, чувствительной ко времени, такой как аукционы или периоды стейкинга.

Эта уязвимость является прямым следствием манипуляций майнеров, отражая их способность влиять на создание блоков. Хотя майнеры не могут устанавливать произвольные временные метки, они имеют некоторый контроль в рамках правил консенсуса. В Ethereum это примерно +/- 900 секунд (15 минут), хотя точный диапазон может варьироваться в зависимости от блокчейна.

Представьте себе аукцион, на котором майнер сдвигает временную метку в пользу своей ставки или ставки своего союзника. Это может привести к несправедливым преимуществам, таким как преждевременное завершение аукциона или манипулирование процессом торгов.

Посмотрите на пример:

pragma solidity ^0.8.0;


// SPDX-License-Identifier: UNLICENSED
contract Auction {
uint public auctionEndTime;
address public highestBidder;
uint public highestBid;
mapping(address => uint) public pendingReturns;
bool public ended;


event BidPlaced(address bidder, uint amount);
event AuctionEnded(address winner, uint amount);


constructor(uint _duration) {
auctionEndTime = block.timestamp + _duration; // Vulnerability!
}


function isAuctionEnded() public view returns (bool) {
return block.timestamp >= auctionEndTime; // Vulnerable comparison!
}


function bid() public payable {
require(!isAuctionEnded(), "Auction has ended");
require(msg.value > highestBid, "Bid not high enough");


if (highestBid > 0) {
pendingReturns[highestBidder] += highestBid;
}


highestBidder = msg.sender;
highestBid = msg.value;
emit BidPlaced(msg.sender, msg.value);
}


function endAuction() public {
require(!ended, "Auction already ended");
require(isAuctionEnded(), "Auction not yet ended");


ended = true;
emit AuctionEnded(highestBidder, highestBid);
}


function withdraw() public returns (bool) {
uint amount = pendingReturns[msg.sender];
if (amount > 0) {
pendingReturns[msg.sender] = 0;


// Use call instead of transfer for better compatibility
(bool success, ) = payable(msg.sender).call{value: amount}("");
require(success, "Transfer failed");
}
return true;
}
}


В этом аукционном контракте время окончания аукциона (auctionEndTime) определяется как block.timestamp + _duration. Майнер может слегка изменить block.timestamp, чтобы досрочно завершить аукцион или отложить его. Он может отложить его настолько, чтобы его ставка была включена в блок, или ускорить его, чтобы исключить конкурирующую ставку.

Вместо того, чтобы полагаться непосредственно на block.timestamp, рассмотрите возможность использования block.number.

pragma solidity ^0.8.0;


contract FixedAuction {
uint256 public auctionEndBlock;
uint256 public blockDuration;


constructor(uint256 _blockDuration) {
auctionEndBlock = block.number + _blockDuration;
blockDuration = _blockDuration; // Duration of auction in blocks
}


function finalizeAuction() public {
require(block.number >= auctionEndBlock, "Auction is not yet over.");
// Distribute funds
}
}
👍4
В этом примере FixedAuction мы используем block.number вместо block.timestamp для определения окончания аукциона. Это делает время проведения аукциона предсказуемым и устойчивым к манипуляциям майнеров с block.timestamp.

Однако майнер может вообще отказаться от добычи блоков, что задержит окончание аукциона, хотя это и является менее прямой формой манипуляции. Кроме того, использование block.number делает аукцион зависимым от скорости создания блоков. Если время блоков несовместимо, продолжительность аукциона может немного варьироваться в реальном времени.

Если требуется более детальное и согласованное время, рассмотрите возможность использования оракула Chainlink, но имейте в виду повышенную сложность и затраты на газ.

В целом рекомендуется избегать разработки функций, чувствительных ко времени с точностью до секунд, поскольку майнеры могут манипулировать временными метками блоков в пределах допустимых значений (обычно ±15 секунд в Ethereum). Смарт-контракты должны реализовывать механизмы, основанные на времени, с достаточными буферными периодами, чтобы предотвратить экономическую эксплуатацию посредством незначительных корректировок временных меток.

#miner #mev
👍6
Атака майнеров. Часть 3

Использует ли контракт свойства блока, такие как временная метка или сложность, для генерации случайных чисел?

Описание: Свойства блока (временная метка, сложность) и другие предсказуемые значения не должны использоваться для генерации случайных чисел, поскольку на них могут влиять или предсказывать майнеры.

Исправление: Вместо этого используйте безопасный источник случайных чисел, такой как Chainlink VRF, схемы commit-reveal или механизм рандомизации с доказанной справедливостью.

Настоящая случайность (randomness) в блокчейне – это сложно. block.timestamp и block.difficulty на первый взгляд могут казаться случайными, но майнеры могут влиять на них, делая результат предсказуемым.

Майнеры могут манипулировать block.timestamp в рамках правил консенсуса, и хотя они имеют очень ограниченный краткосрочный контроль над block.difficulty, полагаться на них в критически важных для безопасности логических операциях – в целом плохая идея.

Представьте себе лотерею, в которой майнер заранее знает выигрышный номер. Не совсем честная игра. Опять же, это относится к манипуляциям майнеров, когда они используют свое влияние для получения преимущества.

Давайте посмотрим на несовершенный лотерейный контракт:

pragma solidity ^0.8.0;


// SPDX-License-Identifier: UNLICENSED


contract Lottery {
address public winner;


function pickWinner() public {
// Vulnerable randomness generation using block.timestamp
uint256 randomNumber = uint256(block.timestamp) % 100;
if (randomNumber == 7) {
winner = msg.sender;
} else {
winner = address(0);
}
}


function getBlockTimestamp() public view returns (uint256) {
return block.timestamp;
}
}


Здесь pickWinner использует block.timestamp для генерации «случайного» числа. Злоумышленник может изменить временную метку блока, содержащего транзакцию pickWinner, повлияв на результат и сфальсифицировав лотерею.

Вместо того, чтобы полагаться на легко манипулируемые свойства блока, используйте безопасный источник случайности, такой как Chainlink VRF, который предоставляет проверяемое и непредсказуемое случайное число, обеспечивая справедливость и предотвращая манипуляции. Свойства блока, такие как block.prevrandao, также лучше, чем block.timestamp, но все же не следует полагаться на них из-за возможности их устаревания.
🔥5
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;


import {VRFConsumerBaseV2Plus} from "@chainlink/contracts/src/v0.8/vrf/dev/VRFConsumerBaseV2Plus.sol";
import {VRFV2PlusClient} from "@chainlink/contracts/src/v0.8/vrf/dev/libraries/VRFV2PlusClient.sol";


/**
* @noscript SecureLottery
* @notice A lottery contract using Chainlink VRF V2.5 for verifiable randomness
* @dev This is an example contract and should not be used in production without proper auditing
*/
contract SecureLottery is VRFConsumerBaseV2Plus {
// Chainlink VRF configuration
uint256 public s_subnoscriptionId;
bytes32 public keyHash = 0x787d74caea10b2b357790d5b5247c2f63d1d91572a9846f780606e4d953677ae; // Sepolia gas lane
uint32 public callbackGasLimit = 100000;
uint16 public requestConfirmations = 3;
uint32 public numWords = 1;


// Lottery state variables
uint256 public randomNumber;
address public winner;
mapping(uint256 => bool) public requestIds;
uint256 public lastRequestId;


// Events
event RandomnessRequested(uint256 requestId);
event WinnerSelected(address winner, uint256 randomNumber);


/**
* @param subnoscriptionId Chainlink VRF subnoscription ID
* @param vrfCoordinator Address of the VRF Coordinator contract
*/
constructor(
uint256 subnoscriptionId,
address vrfCoordinator
) VRFConsumerBaseV2Plus(vrfCoordinator) {
s_subnoscriptionId = subnoscriptionId;
}


/**
* @notice Request random number from Chainlink VRF
* @param useNativePayment Whether to pay in native tokens (true) or LINK (false)
* @return requestId The ID of the randomness request
*/
function requestRandomWinner(bool useNativePayment) external returns (uint256 requestId) {
// Request randomness from Chainlink VRF
requestId = s_vrfCoordinator.requestRandomWords(
VRFV2PlusClient.RandomWordsRequest({
keyHash: keyHash,
subId: s_subnoscriptionId,
requestConfirmations: requestConfirmations,
callbackGasLimit: callbackGasLimit,
numWords: numWords,
extraArgs: VRFV2PlusClient._argsToBytes(
VRFV2PlusClient.ExtraArgsV1({
nativePayment: useNativePayment
})
)
})
);


requestIds[requestId] = true;
lastRequestId = requestId;
emit RandomnessRequested(requestId);
return requestId;
}


/**
* @notice Callback function called by VRF Coordinator when randomness is fulfilled
* @param requestId The ID of the randomness request
* @param randomWords The random words generated by Chainlink VRF
*/
function fulfillRandomWords(
uint256 requestId,
uint256[] calldata randomWords
) internal override {
require(requestIds[requestId], "Request not found");
require(randomWords.length > 0, "Random words array is empty");


// Process the random value
randomNumber = randomWords[0] % 100; // Example: limit to 0-99 range


// Lottery winner selection logic would go here
// For example, if you have participants in an array:
// winner = participants[randomNumber % participants.length];


emit WinnerSelected(winner, randomNumber);
}


/**
* @notice Get the status of a VRF request
* @param requestId The ID of the randomness request
* @return exists Whether the request exists
*/
function getRequestStatus(uint256 requestId) external view returns (bool exists) {
return requestIds[requestId];
}
}
Этот контракт SecureLottery использует Chainlink VRF для генерации действительно случайного числа. Функция requestRandomWords запрашивает случайное число у службы Chainlink VRF. Функция fulfillRandomWords (которая ДОЛЖНА присутствовать при наследовании от VRFConsumerBaseV2Plus) получает случайное число и использует его для определения победителя. Это обеспечивает справедливый и непредсказуемый результат лотереи.

Использование Chainlink VRF влечет за собой внешние зависимости и затраты на газ. Кроме того, требуется настроить подписку на Chainlink VRF и управлять связанными с ней сборами. Однако эти затраты часто являются оправданной платой за повышенную безопасность и справедливость.

#miner #mev
👍82
Атака майнеров. Часть 4

Зависит ли логика контракта от порядка транзакций?

Описание: Майнеры контролируют порядок транзакций и могут использовать это для атак типа «фронт-раннинг», «бэк-раннинг» или «сэндвич».

Устранение: Реализуйте защиту, позволяя пользователям указывать приемлемые результаты, которые отменяют транзакции в случае нарушения.

Майнеры определяют порядок включения транзакций в блок. Хотя майнеры обычно отдают приоритет транзакциям с более высокими ценами на газ, они не обязаны этого делать. Это позволяет злонамеренным майнерам (или сложным ботам) стратегически упорядочивать транзакции в своих интересах. Такое использование может привести к фронтран, бекран или сэндвич атакам. Эти атаки используют манипуляции с порядком транзакций для извлечения выгоды.

Представьте себе децентрализованную биржу (DEX). Майнер видит в мемпуле крупный ордер на покупку определенного токена. Он может вставить свой собственный ордер на покупку перед крупным ордером (фронт-раннинг), что приведет к росту цены. Затем он может вставить свой ордер на продажу после крупного ордера (бэк-раннинг или сэндвич-атака), извлекая прибыль из роста цены, вызванного первоначальной сделкой.

Вот упрощенная уязвимая DEX:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;


import "@openzeppelin/contracts/token/ERC20/ERC20.sol";


// Simple ERC20 token for testing
contract TestToken is ERC20 {
constructor(string memory name, string memory symbol, uint256 initialSupply) ERC20(name, symbol) {
_mint(msg.sender, initialSupply);
}
}


// Simplified DEX that's vulnerable to front-running
contract VulnerableDEX {
TestToken public tokenA;
TestToken public tokenB;
uint public reserveA;
uint public reserveB;


constructor(address _tokenA, address _tokenB) {
tokenA = TestToken(_tokenA);
tokenB = TestToken(_tokenB);
}


// Initialize liquidity
function addLiquidity(uint amountA, uint amountB) external {
tokenA.transferFrom(msg.sender, address(this), amountA);
tokenB.transferFrom(msg.sender, address(this), amountB);
reserveA += amountA;
reserveB += amountB;
}


// Calculate output amount for a given input
function _calculateSwapOutput(address tokenIn, uint amountIn) internal view returns (uint amountOut) {
require(tokenIn == address(tokenA) || tokenIn == address(tokenB), "Invalid token");


bool isTokenA = tokenIn == address(tokenA);


if (isTokenA) {
amountOut = (reserveB * amountIn) / (reserveA + amountIn);
require(amountOut < reserveB, "Insufficient liquidity");
} else {
amountOut = (reserveA * amountIn) / (reserveB + amountIn);
require(amountOut < reserveA, "Insufficient liquidity");
}


return amountOut;
}


// Execute the swap with pre-calculated output
function _executeSwap(address tokenIn, uint amountIn, uint amountOut, address sender) internal {
bool isTokenA = tokenIn == address(tokenA);


if (isTokenA) {
tokenA.transferFrom(sender, address(this), amountIn);


// Update reserves
reserveA += amountIn;
reserveB -= amountOut;


// Transfer output tokens to the user
tokenB.transfer(sender, amountOut);
} else {
tokenB.transferFrom(sender, address(this), amountIn);


reserveB += amountIn;
reserveA -= amountOut;


tokenA.transfer(sender, amountOut);
}
}


// Vulnerable swap function (no minimum output)
function swap(address tokenIn, uint amountIn) external returns (uint amountOut) {
// Calculate the expected output
amountOut = _calculateSwapOutput(tokenIn, amountIn);


// Execute the swap
_executeSwap(tokenIn, amountIn, amountOut, msg.sender);


return amountOut;
}
}
3🔥1
В этом контракте VulnerableDEX функция обмена не имеет защиты от проскальзывания, что делает ее уязвимой для атак типа «сэндвич». Эти атаки являются формой эксплуатации максимально извлекаемой стоимости (MEV), при которой прибыль извлекается путем манипулирования порядком транзакций.

Когда жертва отправляет транзакцию обмена в мемпул, опытные поисковики MEV выявляют возможность и выполняют трехэтапную атаку:

1. Фронтранинг: сначала злоумышленник размещает транзакцию на покупку целевого актива, намеренно завышая цену.

2. Транзакция жертвы: своп жертвы выполняется по этой искусственно завышенной цене, и она получает меньше токенов, чем ожидалось, из-за отсутствия гарантий минимального выхода.

3. Бэкранинг: злоумышленник продает ранее приобретенные токены по более высокой цене, созданной транзакцией жертвы, получая разницу в цене в качестве прибыли.

Хотя любой участник, отслеживающий mempool, может осуществлять атаки типа «сэндвич», производители блоков обладают привилегированными возможностями по упорядочиванию транзакций, что позволяет им осуществлять такие атаки с большей степенью уверенности.

Стратегии смягчения последствий зависят от конкретного контекста, но в целом защита от проскальзывания предотвращает атаки типа «сэндвич», гарантируя, что транзакция жертвы не будет выполнена по невыгодной цене. Позвольте пользователям указывать максимальное проскальзывание, которое они готовы допустить при совершении сделки. Если цена выходит за пределы этого порога, транзакция отменяется. Это защищает пользователей от атак типа «сэндвич».

Вот пример защиты от проскальзывания, которая смягчает последствия атаки типа «сэндвич», описанной в тесте выше.

// Secure swap function with minimum output requirement
function swapWithMinimumOutput(
address tokenIn,
uint amountIn,
uint minAmountOut
) external returns (uint amountOut) {
// Calculate the expected output
amountOut = _calculateSwapOutput(tokenIn, amountIn);


// Check slippage before executing the swap
require(amountOut >= minAmountOut, "Slippage too high");


// Execute the swap
_executeSwap(tokenIn, amountIn, amountOut, msg.sender);


return amountOut;
}


#miner #mev
👍3🔥1
Про современный вайбкоддинг

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

Ситуация начинает меняться очень и очень быстро.

Я много экспериментирую с различными нейронками и редакторами, шаг за шагом продвигаясь и в собственном обучении ml/dl и в разработке пары проектов. И могу с уверенностью сказать, что ИИ сделали просто невероятный шаг в разработке кода за последнее время. С небольшой поправкой: если вы понимаете, что делаете.

Сейчас я использую Cursor IDE в 90% своей работы с кодом, с использованием Claude 4 (4.1, 4.5). Также экспериментирую с Qwen-Coder, CLM-4.5 (4.6) и GPT5. Работу с CLI пока не довелось хорошенько погонять...

И знаете что? В рамках IDE они действительно могут понимать контекст задачи и писать хороший код. При этом вы сами должны понимать, что ИИ пытается реализовать и помещать его в определенные рамки, как например, не добавлять не нужную отладку, не создавать тестовые файлы и т.д. Другими словами, ИИ будет делать то, что вы ему говорить, а не что ожидаете от него в своих мыслях.

Все идет к тому, что скоро у разработчиков пойдет смещение со стороны написания кода, на его аудит и чтение. Зачем тратить время на написание функции, когда я могу дать четкие указания ИИ на написание функции с нужными параметрами и проверками, а затем проаудировать код?

На мой взгляд, скоро будет важнее иметь теоретические знания, чем практические (где-то в соотношении 80% - 20%). При этом ваша теория должна быть настолько хороша, чтобы вы могли объяснить ее малолетнему ребенку.

Складывается таке ощущение, что скоро будут появляться больше информации с примерами функций или программ, с детальными комментариями к промту и требованиям, чтобы этот код написать.

Например, вместо примера контракта обычного токена, будет идти небольшое описание этого контракта и промт для ИИ, в котором будет расписана структура этого контракта: какие функции должны быть включены, что они должны делать, какие проверки в этих функциях должны быть, как они должны взаимодействовать друг с другом и т.д. При этом важно учитывать то, что ИИ уже будут знать эталонные примеры контрактов из библиотек Open Zeppelin, Uniswap или Chainlink.

Если вы думаете, что станет больше работы для Интернов и Джунов, то спешу вас огорчить.

Хороший промт может написать только уверенный мидл, как и проверить его написание после ИИ.

Давно я писал, что с каждым годом требования к соискателям даже на начальные позиции будут все более широкие: там где сначала требовалось знать только Solidity, потом потребовались знания написания тестов, потом базовой безопасности смарт контрактов, а теперь еще и навыки работы с ИИ и промтинга.

ИИ облегчит разработку в рамках "времени", но не профессиональных знаний.

#ai #vibecoding #solidity
👍7
Контекст смарт контракта для AI

Сегодня просто решил поделиться с вами своими наблюдениями, которые я получил создавая небольшой проект на стыке AI/Web3.

В рамках задачи мне нужно было каким-то образом сжать смарт контракт для отправки его в модель LLM для анализа. Само сжатие потребовалось, чтобы уместить один или несколько смарт контрактов в рамки контекстного окна модели. Для тех, кто не в курсе, далее чуть поясню суть проблемы.

Несмотря на то, что современные модели могут иметь контекстные окна (тот объем информации, который они могут обработать за раз) от 200 000 до 1 000 000 токенов, активное внимание хорошо работает только примерено до 100 000 токенов. Другими словами, до этого объема они могут эффективно следовать инструкциям и "не забывать" условия и суть задачи.

Запросы пользователя в чатах не такие уж и большие, зачастую всего 50-250 токенов, поэтому общение в чате может быть достаточно долгим, если не просить модель отвечать развернуто.

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

пользовательское сообщение + системный промт + контракт + документацию

Детально написанный системный промт может занимать 5к-15к токенов, длинный контракт около 8к-10к, документация и того больше. В итоге один запрос для хорошего анализа смарт контракта может занимать около 30к-40к токенов. И вроде бы ок, если не думать, что для более глубокого анализа связей между функциями нужно добавить еще 2-3 контракта, которые наследуются в первом. И тут уже контекст растет значительно!

Поэтому мне и нужно было как-то сжимать контракты и документацию, чтобы уменьшать количество используемых токенов, хотя бы на 30%-40%, а лучше и больше.

К сожалению, удаление комментариев из файла контракта, уменьшение пробелов и переносов строк, могут бать максимум 5%-8% сокращения. И этого мало.

Общение с ИИ также не сильно помогает: в большинстве своем они дают самые очевидные советы.

Однажды мне пришла мысль, что можно заменить самые часто повторяющиеся слова в контрактах (function, public, view, balanceOf) на какие-либо символы - типа тех же смайлов, так как по уверениям ChatGPT, смайлы занимают всего один токен.

P.S. Тут к слову сказать, что зачастую 1 токен равен около 3,5-4 символам текста, но для разных ИИ это могут быть разные значения.

И вроде как логично, 1 смайл - 1 токен. Но все оказалось сложнее.

Сначала нужно было узнать те самые популярные слова в контактах. Благо в сети можно найти отличные подборки контрактов, которые использовали в различных датасетах. Я получил около 130 000 реальных контрактов.

Затем с помощью простого скрипта на Python, я собрал самые повторяющиеся слова, которых вышло около 150 000 всего. Слов которые повторялись более 500 раз на эту подборку было около 10 000. Их я и решил использовать.

Далее я написал небольшой скрипт, который используя популярные библиотеки для подсчета токенов (antropic, openai, huggingface и базовый - 1 токен/4 символа), прогнал все 10 000 слов.

Далее, собрал все PUA / emoji / редкие символы (UU+E000–U+F8FF) и также прогнал их через скрипт подсчета токенов.

И тут было мое разочарование. Оказалось, что большинство популярных слов в смарт контрактах и так составляют всего 2-3 токена, как и смайлы!

OnlyOwner, balanceOf и другие популярные переменные занимают столько же места в контексте, что и :)!

В данном эксперименте я не учет способ обучения модели на данных! Когда, грубо говоря, GPT обучается на смарт контрактах и видит миллионы из них, то самые часто употребляющиеся слова "сливаются" в меньшее количество токенов, и не зависит от количества символов в слове! Другими словами, модель сама себя оптимизирует по контексту, и таким образом программный код занимает минимально возможное количество токенов. (На самом деле процесс немного сложнее, но суть примерно та же).

И от этой идеи пришлось отказаться...

Но есть пара других: кардинально другой подход, который на первых тестах показал сокращение количества токенов на 45%! Экспериментирую с ним дальше!
👍8🤔1