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

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

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

В остальном — всё в порядке. Если понадобится, я всегда найду время ответить в чате.

#timeout
🐳51🤯1
Погружение в Core Solidity. Часть 1

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

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

А пока, давайте поговорим об обновлении языка Solidity - Core Solidity, о котором я писал не так давно. Разработчики выкатили небольшое описание функций системы, которые мы рассмотрим на этой неделе.

P.S. Далее будет идти перевод статьи от имени разработчиков.

Solidity — это самый широко используемый язык для написания смарт-контрактов. Он надёжен, заслуживает доверия и сегодня обеспечивает сохранность активов на сумму в сотни миллиардов долларов. Мы гордимся этим успехом и безупречной репутацией генерации безопасного кода. Однако пользователи Solidity прекрасно осознают некоторые его ограничения. Система типов зачастую недостаточно выразительна: она не позволяет создавать многократно используемый код библиотек или гарантировать ключевые свойства безопасности. Язык почти не поддерживает вычисления на этапе компиляции. Многие функции реализованы несогласованно или не всегда работают так, как ожидается.

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

Core Solidity — наше решение этой проблемы. Это полная переработка системы типов и фронтенд/мидл-энд компилятора Solidity, которая позволит:

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

Помимо расширения и развития языка, мы также собираемся убрать или переработать некоторые существующие функции. Уже точно решено, что мы полностью уберём наследование. Другие изменения пока менее определены, но мы рассматриваем возможность замены или переработки таких механизмов, как try/catch, библиотеки, указатели на функции, преобразование типов и указание мест хранения данных.

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

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

Хотя прототип уже работает, он пока не оптимизирован для удобства пользователей. Мы активно продолжаем работу над прототипом, и новые функции будут постепенно появляться в репозитории проекта для обратной связи и экспериментов. Мы с нетерпением ждём ваших комментариев!
Примечание о синтаксисе

Большая часть проделанной на сегодняшний день работы была сосредоточена на проектировании и реализации системы типов и связанного с ней конвейера генерации кода вплоть до Yul. Чтобы не увязнуть в бесконечных спорах о деталях синтаксиса и как можно скорее проверить наши ключевые идеи на рабочей реализации, мы решили использовать временный (провизорный) синтаксис. До релиза можно ожидать значительных изменений. В настоящее время мы стремимся в итоге максимально приблизить синтаксис Core Solidity к синтаксису классического Solidity. Что касается нового синтаксиса, то окончательная его версия, скорее всего, будет ближе к таким языкам, как TypeScript или Rust.

Новые языковые возможности

Core Solidity заимствует идеи из чистых функциональных языков программирования (например, Haskell, Lean), а также из современных системных языков (например, Rust, Zig). Мы расширяем Solidity следующими новыми возможностями:

- Алгебраические типы данных (известные также как суммы и произведения типов) и сопоставление с образцом (pattern matching)
- Обобщения (generics) / параметрический полиморфизм
- Трейты (traits) / классы типов (type classes)
- Вывод типов (type inference)
- Функции высшего порядка и анонимные функции
- Вычисления на этапе компиляции

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

Мы продолжим поддерживать низкоуровневый доступ к EVM, который часто необходим в промышленных реализациях: встроенная ассемблерная вставка (assembly) останется базовой языковой конструкцией, и мы расширим блоки ассемблера возможностью напрямую вызывать функции, определённые на высокоуровневом языке. Пользователи смогут отключать встроенные абстракции (например, автоматическую генерацию диспетчеризации контрактов, декодирование ABI, генерацию стандартной схемы хранения данных), следуя философии «плати только за то, чем пользуешься», характерной для таких языков, как Rust и C++.

Далее поговорим подробнее и с примерами.

#core
🔥6🤯3🤔2
Погружение в Core Solidity. Часть 2

Алгебраические типы данных и сопоставление с образцом

Алгебраические типы данных (ADT, Algebraic Data Types) предоставляют принципиальную основу для моделирования данных за счёт комбинирования суммарных (sum) и произведённых (product) типов. Суммарные типы являются расширением перечислений (enums) из классического Solidity. Они представляют исключающие друг друга варианты: значение принадлежит ровно одному из возможных вариантов. Произведённые типы объединяют несколько значений в структурированные кортежи. На основе этих двух примитивов можно конструировать точные типы, делающие недопустимые состояния полностью невозможными для представления, что позволяет системе типов обеспечивать соблюдение инвариантов полностью на этапе компиляции.

Начнём с очень простого типа:

data Bool = True | False


Левая часть приведённого выше выражения определяет имя нового типа (Bool), а правая часть задаёт множество значений, составляющих тип Bool (True или False).

С помощью ADT также можно реализовать те же самые паттерны, что и пользовательские типы-значения (User Defined Value Types) в классическом Solidity. Например, значение с фиксированной точкой и 18 знаками после запятой («wad») можно представить следующим образом:

data wad = wad(uint256)


Тип wad (слева) имеет единственный конструктор значений wad (справа), который хранит значение типа uint256 в качестве своего внутреннего представления. Имена типов и конструкторы значений находятся в отдельных пространствах имён, поэтому могут совпадать. Простые обёрточные типы подобного рода будут полностью удалены компилятором при трансляции в Yul, то есть тип wad будет иметь точно такое же представление во время выполнения, как и uint256.

Теперь можно определить процедуру умножения чисел с фиксированной точкой с проверкой типов. Для этого потребуется извлечь внутреннее значение uint256, произвести над ним необходимые операции и обернуть результат в новый конструктор wad. Для распаковки воспользуемся сопоставлением с образцом (pattern matching). Сопоставление с образцом — это механизм управления потоком выполнения, позволяющий деконструировать и анализировать данные по их структуре. Вместо громоздких цепочек if-else можно писать декларативные выражения, полностью перебирающие все возможные значения проверяемого типа.

let WAD = 10 ** 18;

function wmul(lhs : wad, rhs : wad) -> wad {
match (lhs, rhs) {
| (wad(l), wad(r)) => return wad((l * r) / WAD);
}
}


Тип AuctionState, ниже, имеет четыре альтернативных конструктора значений:

- NotStarted указывает, что аукцион ещё не начался, и хранит резервную цену;
- Active означает, что аукцион идёт, и хранит текущую максимальную ставку и адрес того, кто её сделал;
- Ended представляет успешно завершившийся аукцион с максимальной ставкой и адресом победителя;
- Cancelled описывает отменённый аукцион и хранит максимальную ставку и адрес предполагаемого победителя на момент отмены.

Теперь можно определить функцию processAuction, которая изменяет состояние аукциона в зависимости от текущего состояния и значения msg.value. Выражение match позволяет выполнить исчерпывающий разбор всех возможных состояний. Случай _ в конце конструкции match является обработчиком по умолчанию для всех оставшихся состояний, которые явно не были перечислены. Компилятор гарантирует полноту такого разбора, требуя, чтобы каждое возможное состояние обрабатывалось ровно один раз.

function processAuction(state: AuctionState) -> AuctionState {
match state {
| NotStarted(reserve) =>
require(msg.value >= reserve);
return Active(msg.value, msg.sender);
| Active(currentBid, bidder) =>
require(msg.value > currentBid);
transferFunds(bidder, currentBid);
return Active(msg.value, msg.sender);
| _ => return state;
}
}


#core
👍8🔥2
Погружение в Core Solidity. Часть 3

Обобщения и классы типов

Core Solidity вводит два новых механизма для повторного использования кода и полиморфизма: обобщения (generics) и классы типов (иногда также называемые трейтами, traits).

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

forall T . function identity(x : T) -> T {
return x;
}


Здесь forall вводит новую переменную-тип T, область видимости которой ограничена определением функции.

Можно также определять обобщённые типы. Например, следующий тип Result, параметризованный типом полезной нагрузки в случае ошибки:

data Result(T) = Ok | Err(T)


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

Класс типов — это просто спецификация интерфейса. Рассмотрим, например, определение класса типов, которые поддерживают операцию умножения:

forall T . class T:Mul {
function mul(lhs : T, rhs : T) -> T;
}


Вместо конкретной функции wmul, которую мы определили выше для нашего типа wad с фиксированной точкой, более идиоматично создать экземпляр (в терминологии Rust — impl) класса типов Mul для wad. Это даёт единообразный синтаксис умножения для всех типов и позволяет использовать wad в функциях, обобщённых над любыми типами, реализующими Mul:

instance wad:Mul {
function mul(lhs : wad, rhs : wad) -> wad {
return wmul(lhs, rhs);
}
}


Если мы хотим написать функцию, принимающую любой тип, для которого определён экземпляр Mul, необходимо добавить ограничение в сигнатуру:

forall T . T:Mul => function square(val : T) -> T {
return Mul.mul(val, val);
}


Простые обёрточные типы вроде wad встречаются очень часто. Один из особенно полезных классов типов при работе с ними — Typedef:

forall T U . class T:Typedef(U) {
function abs(x : U) -> T;
function rep(x : T) -> U;
}


Функции abs (абстрагирование) и rep (представление) позволяют единообразно преобразовывать обёрточные типы во внутренние и наоборот, избегая синтаксического шума, связанного с необходимостью использовать сопоставление с образцом каждый раз при распаковке значения. Экземпляр для wad выглядел бы так:

instance wad:Typedef(uint256) {
function abs(u : uint256) -> wad {
return wad(u);
}

function rep(x : wad) -> uint256 {
match x {
| wad(u) => return u;
}
}
}


Обратите внимание: параметры, следующие после имени класса (например, U в определении Typedef выше), являются «слабыми» — их значение однозначно определяется значением параметра T. Если вы знакомы с Haskell или Rust, то это по сути ассоциированный тип (associated type) (хотя, для тех, кто разбирается в системах типов, реализовано это с помощью ограниченной формы функциональных зависимостей). Проще говоря, для wad можно определить только один экземпляр Typedef: компилятор не разрешит одновременно объявить и wad:Typedef(uint256), и wad:Typedef(uint128). Это ограничение делает вывод типов значительно более предсказуемым и надёжным, избегая многих неоднозначностей, присущих полноценным многопараметрическим классам типов.
2
В качестве реального примера того, как обобщения и ограничения через классы типов помогают устранить шаблонный и повторяющийся код, сравним комбинаторный взрыв перегрузок, необходимых для реализации console.log в библиотеке forge-std, с одной обобщённой функцией в Core Solidity, которая покрывает функциональность всех перегрузок с одним аргументом из оригинальной библиотеки. Слово word в этой реализации обозначает низкоуровневый тип, представляющий переменную на языке Yul, и является единственным типом, который можно передавать в блоки assembly и получать из них.

forall T . T:ABIEncode => function log(val : T) {
let CONSOLE_ADDRESS : word = 0x000000000000000000636F6e736F6c652e6c6f67;
let payload = abi_encode(val);

// извлекаем внутреннее представление payload как word
let ptr = Typedef.rep(payload);

assembly {
pop(
staticcall(
gas(),
CONSOLE_ADDRESS,
add(ptr, 32),
mload(ptr),
0,
0
)
)
}
}


Подобно Rust и Lean, все вызовы классов типов и обобщённых функций полностью мономорфизируются (monomorphized) на этапе компиляции. Это означает, что полиморфные функции не несут накладных расходов во время выполнения по сравнению с полностью конкретизированными функциями. Хотя это действительно приводит к тому, что скомпилированный код для EVM может содержать несколько специализированных версий одной и той же обобщённой функции, это не увеличивает размер бинарного файла по сравнению с классическим Solidity, где для получения эквивалентной функциональности в любом случае потребовалось бы определять несколько отдельных функций. Мы считаем такой компромисс полностью оправданным для нашей предметной области.

#core
👍61
Погружение в Core Solidity. Часть 4

Функции высшего порядка и анонимные функции

Функции обладают статусом «первоклассных» объектов в системе типов, что позволяет использовать их в качестве параметров, возвращаемых значений и присваиваемых сущностей.

В качестве примера рассмотрим следующий фрагмент, реализующий пользовательскую декодировку ABI для тройки логических значений из одного слова:

forall T . function unpack_bools(fn : (bool, bool, bool) -> T) -> ((word) -> T) {
return lam (bools : word) -> {
let wordToBool = lam (w : word) { return w > 0; };

// extract the right-most bit from `bools`
let b0 = wordToBool(and(bools, 0x1));

// shift `bools` by one and extract the right-most bit
let b1 = wordToBool(and(shr(1, bools), 0x1));

// shift `bools` by two and extract the right-most bit
let b2 = wordToBool(and(shr(2, bools), 0x1));

return fn(b0, b1, b2);
};
}


Функция unpack_bools реализует пользовательскую декодировку ABI. Она является функцией высшего порядка, которая «оборачивает» входную функцию, принимающую три отдельных логических значения и возвращающую значение произвольного типа T, извлекая аргументы из трёх младших битов входного слова. Такой пример невозможно реализовать в классическом Solidity, даже с использованием модификаторов, поскольку они не могут изменять аргументы, передаваемые внутрь оборачиваемой функции.

Кроме того, поддерживается определение (некурсивных) анонимных функций с помощью ключевого слова lam. Функции, определённые таким образом, могут захватывать значения из области видимости, в которой они объявлены. В качестве примера рассмотрим вспомогательную функцию для тестирования, подсчитывающую количество вызовов произвольной функции:

forall T U . function count_calls(fn : (T) -> U) -> (memory(word), (T) -> U) {
let counter : memory(word) = allocate(32);
return (counter, lam (a : T) -> {
counter += 1;
return fn(a);
});
}


Реализация здесь аналогична тому, как это сделано в системных языках, таких как Rust и C++: компилятор генерирует уникальный тип для каждой анонимной функции, содержащей захваченные значения, а эти уникальные типы становятся вызываемыми за счёт принадлежности к специальному типовому классу «вызываемых» (invokable), подобно трейту Fn в Rust. Такой подход обеспечивает высокую эффективность с точки зрения затрат газа во время выполнения.

Вывод типов

Core Solidity поддерживает вывод типов почти в любом контексте. Аннотации типов обычно требуются только тогда, когда это желательно для улучшения читаемости или понимания кода. Алгоритм вывода типов разрешим, а ситуации, в которых возникает неоднозначность и требуется явная аннотация, крайне ограничены. Благодаря этому удаётся избавиться от большого количества синтаксического шума, присущего классическому Solidity.

Например, присваивание выражения переменной в классическом Solidity нередко приводит к избыточным аннотациям, даже если типы уже присутствуют в самом выражении:

(bytes memory a, bytes memory b) = abi.decode(input, (bytes, bytes));


То же самое определение в Core Solidity выглядит значительно чище:

let (a, b) = abi.decode(input, (uint256, uint256));


Ещё одна частая причина раздражения при работе с классическим Solidity — синтаксический шум при определении литералов массивов. Рассмотрим следующий фрагмент:

uint256[3] memory a = [1, 2, 3];


Это объявление отвергается компилятором классического Solidity со следующей ошибкой:

Error: Type uint8[3] memory is not implicitly convertible to expected type uint256[3] memory.


Причина ошибки заключается в том, что классический Solidity реализует ограниченную и специализированную форму вывода типов для литералов массивов: тип элементов массива определяется как тип первого выражения в списке, к которому можно неявно привести все остальные элементы (в данном случае — uint8). Затем компилятор выдаёт ошибку несоответствия типов при попытке присвоить это значение переменной несовместимого типа.
4
Чтобы приведённое выше определение было принято, приходится добавлять неочевидное приведение типа к первому элементу массива:

uint256[3] memory a = [uint256(1), 2, 3];


Алгоритм вывода типов на основе ограничений в Core Solidity гораздо более общий и позволяет опустить такое приведение:

uint256[3] memory a = [1, 2, 3];


#core
👍42
Погружение в Core Solidity. Часть 5

SAIL, десахаризация и стандартная библиотека

Помимо расширения поверхностного языка, переход на Core Solidity также введёт новый промежуточный язык среднего уровня, доступный пользователям: SAIL (Solidity Algebraic Intermediate Language — Алгебраический промежуточный язык Solidity). Это и есть «ядро» Core Solidity. SAIL представляет собой максимально упрощённый язык, на котором можно выразить всё разнообразие высокоуровневых конструкций, присутствующих в классической Solidity. Он состоит из следующих примитивных конструкций:

- Функции
- Контракты
- Блоки ассемблера (Yul)
- Объявление и присваивание переменных SAIL
- Выражение условного ветвления с коротким замыканием (if-then-else)
- Алгебраические типы данных и сопоставление с образцом
- Классы типов (type classes)
- Обобщения (generics)

Переменная SAIL концептуально похожа на переменную Yul: компилятор связывает её с ячейкой в стеке EVM. В SAIL существует единственный встроенный тип (word), диапазон значений которого совпадает с типами bytes32 или uint256 в классической Solidity и который семантически можно рассматривать как тип, соответствующий одному слоту стека EVM. Контракты в SAIL крайне низкоуровневы — по сути, это просто точки входа времени выполнения и инициализационного кода (initcode).

Хотя в текущей реализации SAIL используется Yul в качестве языка ассемблера, с теоретической точки зрения этот выбор в значительной степени произволен, и вместо него можно было бы использовать, например, ассемблер на основе RISC-V.

Мы уверены, что SAIL достаточно выразителен, чтобы реализовать все высокоуровневые функции и типы языка как комбинацию определений из стандартной библиотеки и проходов десахаризации — то есть синтаксических преобразований времени компиляции в примитивы SAIL. Core Solidity, таким образом, представляет собой SAIL, дополненный дополнительным «синтаксическим сахаром» и библиотеками. Он схож с Yul в своей двойной роли как промежуточного представления компилятора и низкоуровневого языка, доступного пользователю, и все примитивы SAIL будут непосредственно доступны при написании кода на Core Solidity. Подобный подход к построению языков широко применяется в других областях, требующих высокой надёжности (например, в системах автоматического доказательства теорем), и, по нашему мнению, он приносит существенные преимущества как для пользователей языка, так и для безопасности и корректности его реализации.

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

Авторы библиотек получат практически ту же выразительную мощность, что и разработчики языка, и смогут создавать абстракции, ощущающиеся как встроенные в сам язык («язык на основе библиотек»). Появится возможность определять и использовать альтернативные реализации стандартной библиотеки или полностью отключать стандартную библиотеку. При отключённой стандартной библиотеке можно будет писать код на Core Solidity с почти таким же уровнем контроля, как при использовании низкоуровневых ассемблерных языков вроде Yul или Huff, но при этом с современной и выразительной системой типов, основанной на математически строгих принципах.
3
Кроме того, мы ожидаем, что появление SAIL существенно упростит расширение и улучшение языка. Во многих случаях достаточно будет внести глубокие улучшения простым пул-реквестом в стандартную библиотеку. Когда же потребуется новая синтаксическая конструкция или новый проход десахаризации, мы рассчитываем, что их будет значительно проще прототипировать и специфицировать на SAIL без необходимости глубокого понимания внутренностей компилятора. Мы надеемся, что SAIL и Core Solidity позволят нам перейти к процессу разработки изменений в высокоуровневый язык и стандартную библиотеку, основанному на сообществе и использующему RFC-подобную модель.

#core
👍5
Погружение в Core Solidity. Часть 6

Пользовательский ABI-кодировщик в пользовательском пространстве

Рассмотрим, как SAIL можно использовать для реализации высокоуровневых возможностей Core Solidity. Функция abi.encode — сложная и чрезвычайно обобщённая; в классическом Solidity она предоставляется как встроенная в компилятор. Полноценную реализацию этой функции на самом языке Solidity создать невозможно из-за рекурсивной природы спецификации ABI и, как следствие, бесконечного множества выразимых типов. Представленная здесь реализация сравнительно компактна, однако использует некоторые более продвинутые шаблоны и возможности. Мы хотим подчеркнуть, что обычные пользователи Solidity смогут эффективно работать, опираясь на уже имеющиеся знания, и при этом им не придётся сталкиваться с подобными низкоуровневыми внутренними деталями. В то же время мы надеемся, что продвинутые разработчики и авторы библиотек обрадуются новым возможностям, которые открывают эти возможности.

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

uint256

Для начала сконструируем тип uint256. В классическом Solidity определение этого типа и всех связанных с ним операций является встроенной языковой конструкцией. В SAIL же этот тип полностью определяется средствами самого языка как простая обёртка над значением типа word. Мы также определяем для него экземпляр Typedef:

data uint256 = uint256(word);

instance uint256:Typedef(word) {
function abs(w : word) -> uint256 {
return uint256(w);
}

function rep(x : uint256) -> word {
match x {
| uint256(w) => return w;
}
}
}


Память и байты

Мы можем создавать типы, представляющие указатели на различные области данных EVM, путём обёртки значения типа word. Обратите внимание, что в приведённом фрагменте параметр типа у указателя memory является фантомным (phantom): он присутствует только в типе, но не упоминается ни в одном конструкторе значений. Такой подход широко используется в языках семейства ML, таких как Haskell или Rust, позволяя обеспечивать ограничения на этапе компиляции без каких-либо накладных расходов во время выполнения.

data memory(T) = memory(word)


Тип bytes в классическом Solidity представляет собой плотно упакованный массив байтов, размер которого известен только во время выполнения. Классический Solidity всегда требует указания области данных для значений типа bytes, поэтому в Core Solidity мы определяем его как пустой тип без конструкторов значений. Пустые типы могут использоваться только для инстанцирования фантомных параметров типов. Это означает, что, как и в классическом Solidity, экземпляры типа bytes не могут находиться в стеке.

data bytes;


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

#core
👍4
Погружение в Core Solidity. Часть 7

Тип Proxy

Последним элементом, необходимым для реализации abi.encode, является тип Proxy:

data Proxy(T) = Proxy;


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

Хотя Proxy может показаться довольно экзотическим инструментом, он чрезвычайно полезен и даёт нам значительный контроль над выводом типов и выбором экземпляров классов типов без необходимости передавать данные во время выполнения там, где они не нужны. Такой подход часто используется как в Haskell (где он тоже называется Proxy), так и в Rust (std::marker::PhantomData).

abi.encode

Теперь мы готовы реализовать функцию abi.encode из классического Solidity на языке SAIL. Начнём с определения класса типов для метаданных, связанных с ABI. Заметим, что поскольку этому классу не нужно знать фактическое значение передаваемого типа, мы используем Proxy, чтобы сделать реализацию максимально лёгкой.

forall T . class T:ABIAttribs {
// how many bytes should be used for the head portion of the ABI encoding of `T`
function headSize(ty : Proxy(T)) -> word;
// whether or not `T` is a fully static type
function isStatic(ty : Proxy(T)) -> bool;
}

instance uint256:ABIAttribs {
function headSize(ty : Proxy(uint256)) -> word { return 32; }
function isStatic(ty : Proxy(uint256)) -> bool { return true; }
}


Теперь определим ещё один класс, отвечающий за низкоуровневую запись в память. Приведённый здесь класс содержит некоторые дополнительные детали, необходимые для кодирования составных и динамических типов, которые не требуются для простого случая uint256, рассматриваемого сейчас. Мы показываем полную сложность, чтобы продемонстрировать, что наша система способна справляться и с более сложными случаями.

// types that can be abi encoded
forall T . T:ABIAttribs => class T:ABIEncode {
// abi encodes an instance of T into a memory region starting at basePtr
// offset gives the offset in memory from basePtr to the first empty byte of the head
// tail gives the position in memory of the first empty byte of the tail
function encodeInto(x : T, basePtr : word, offset : word, tail : word) -> word /* newTail */;
}

instance uint256:ABIEncode {
// a unit256 is written directly into the head
function encodeInto(x : uint256, basePtr : word, offset : word, tail : word) -> word {
let repx : word = Typedef.rep(x);
assembly { mstore(add(basePtr, offset), repx) }
return tail;
}
}


Наконец, мы можем определить высокоуровневую функцию abi_encode, которая занимается первоначальным выделением памяти и обновлением указателя на свободную память (реализация вспомогательных низкоуровневых функций get_free_memory и set_free_memory опущена для краткости):

// top level encoding function.
// abi encodes an instance of `T` and returns a pointer to the result
forall T . T:ABIEncode => function abi_encode(val : T) -> memory(bytes) {
let free = get_free_memory();
let headSize = ABIAttribs.headSize(Proxy : Proxy(T));
let tail = ABIEncode.encodeInto(val, free, 0, Add.add(free, headSize));
set_free_memory(tail);
return memory(free);
}


#core
👍2
Погружение в Core Solidity. Финал

Совместимость и взаимодействие

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

Как и в предыдущих крупных обновлениях Solidity, совместимость на уровне ABI будет сохраняться между версиями. Это позволит отдельным контрактам, написанным на несовместимых версиях языка, взаимодействовать друг с другом и сосуществовать в рамках одного проекта (аналогичная стратегия применяется в Rust с их функцией «Editions»). Мы также изучаем возможность более глубокого взаимодействия, выходящего за рамки только ABI контрактов. Предполагается, что хотя бы свободные функции и определения интерфейсов можно будет совместно использовать между версиями языка.

Хотя изменения затронут как синтаксис, так и семантику, мы намерены свести их к минимуму и применять только в тех случаях, когда это строго необходимо или приносит существенные преимущества, оправдывающие затраты на миграцию. Мы ожидаем, что простой код без наследования будет выглядеть и ощущаться практически одинаково в обеих версиях языка, с лишь незначительными синтаксическими отличиями (в основном — переход от префиксной записи типов к постфиксной). Мы также рассматриваем возможность переработки или замены некоторых функций, которые на практике оказались проблемными или ограничивающими (например, try/catch, библиотеки, указатели на функции, области данных). Пользователи могут ожидать, что для адаптации кода, использующего эти возможности, потребуются умеренные изменения. Разумеется, код, активно использующий наследование, потребует наиболее значительных переделок.

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

Избежать раскола по типу «Python 2 → Python 3» — наша главная задача. Мы считаем, что обновления должны быть управляемыми и возможными для поэтапного внедрения.

Путь к использованию

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

У нас уже есть прототип, реализованный в отдельном репозитории: solcore. Мы можем выполнять проверку типов программ на SAIL и имеем конвейер генерации кода до уровня Yul. Однако до окончательной фиксации системы типов нам ещё предстоит реализовать как минимум вычисления на этапе компиляции и модульную систему. У нас есть базовая стандартная библиотека и достаточное количество этапов «десахаризации» (desugaring) для реализации самых фундаментальных возможностей классического Solidity. Мы уже можем генерировать контракты, совместимые по ABI, с поддержкой диспетчеризации, кодирования/декодирования ABI и доступа к хранилищу.

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

Как только мы убедимся в стабильности прототипа, работа разделится на два параллельных направления:
1. Продакшн реализация: мы перепишем проверку типов, этапы десахаризации и генерацию Yul на системном языке (например, Rust, C++ или Zig) и интегрируем это в основной компилятор solc. Эта реализация будет ориентирована на корректность, производительность и обеспечение максимально качественных диагностических сообщений и ошибок.

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

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

#core
👍2
Вышел Solidity 0.8.31

Команда Solidity объявила о выходе компилятора Solidity версии 0.8.31. Обновление приносит поддержку новых возможностей EVM, представленных в сетевом апгрейде Fusaka, расширяет функциональность спецификаторов раскладки хранилища и запускает первый этап отказа от устаревших возможностей, которые будут окончательно удалены в версии 0.9.0. Кроме того, теперь официально публикуются сборки компилятора для Linux на архитектуре ARM.

Одним из ключевых изменений стало то, что версия EVM с кодовым названием osaka теперь используется по умолчанию. При необходимости разработчики по-прежнему могут указать более старую версию виртуальной машины через настройки компилятора. В новой версии также добавлена поддержка опкода CLZ, реализующего стандарт EIP-7939. Эта инструкция позволяет считать количество ведущих нулей в 256-битном слове и открывает новые возможности для оптимизаций, битовых операций, алгоритмов сжатия и работы со структурами данных на уровне приложений.

В ближайшее время этот опкод найдет активное применение в популярных библиотеках, включая solady и OpenZeppelin, где сможет заменить существующие реализации вроде Math.clz(). Пока в самом компиляторе область применения CLZ ограничена, но команда изучает способы использовать его для будущих оптимизаций генерации байткода.

С точки зрения инфраструктуры релиза произошло важное обновление: начиная с этой версии, Solidity официально выпускается в виде бинарных сборок для Linux на ARM. Ранее такие версии существовали либо в виде сборок под macOS, либо в виде самостоятельной компиляции из исходников. Теперь ARM-билды встроены в систему CI и проходят тот же цикл тестирования, что и остальные платформы, гарантируя идентичность байткода и метаданных на всех архитектурах.

Также введён формат предварительных релизов. Если раньше существовали только ночные сборки и полноценные релизы, то теперь появились pre-release версии, позволяющие получать доступ к новым фичам раньше официального релиза. Именно в pre-release впервые стала доступна поддержка CLZ, и эта практика будет использоваться дальше для постепенного внедрения экспериментальных возможностей.

Одновременно команда начала оптимизировать каналы распространения компилятора. В частности, официально прекращена поддержка Ubuntu PPA, так как этот канал оказался маловостребованным. Docker-сборки пока сохраняются, но в будущем тоже могут быть убраны, если их использование останется незначительным. При этом контейнеры уже перенесены из DockerHub в реестр GitHub, и новые версии будут публиковаться именно там.

Наконец, версия 0.8.31 открывает фазу активной подготовки к релизу 0.9.0, который станет несовместимым с предыдущими версиями. В компилятор добавлены предупреждения об устаревании send() и transfer(), устаревшего ABI coder v1, виртуальных модификаторов, сравнений контрактов без явного приведения к адресу и специального комментария memory-safe-assembly. Всё это сигнализирует о переходе Solidity к более строгой типизации, более прозрачной семантике и сокращению исторически сложных и небезопасных конструкций, которые долгое время тянулись из ранних версий языка.

#solidity
1👍84
Vibe codding и ваши идеи

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

Сегодня просто хочу поделиться своими заметками про такой подход.

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

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

Например, недавно меня просто позабавила идея объединения крипто торговли и доски канбан (типа Trello). Я посчитал, что было бы забавно и просто для новичков, вместо отслеживания бесконечных графиков просто перетаскивать карточки с одного столбца в другой, и тем самым выполнять действия покупка/продажа крипты. В итоге, с помощью нескольких запросов в Lovable (ИИ сервис для генерации фронтенд) появился такой концепт:

Сайт CryptoBoard

Тут просто можно перетаскивать карточки с места нам место. Больше ничего.

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

Такие сайты как Lovable, Replit, Manus могут помочь быстро составить прототип вашей идеи. Вы также можете использовать LMArena и разные описания того, что вы хотите, для генерации страниц проекта разными нейронками, выбирая наиболее удачные варианты. Сохраните их в отдельную папку.

Затем в современных редакторах кода с ИИ (Cursor, Windsurf, Antigravity), создайте базовый проект и попросите нейронку перенести дизайн сгенерированный ранее, на ваш новый проект. Так потихоньку можно создать базовый фронтенд.

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

Так, для меня лучше всего сработали Python+FastAPY+Redis. По каждому из них я брал дополнительное обучение онлайн.

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

Кроме того, в конце вы так же можете попросить модель, типа Claude 4.5, проанализировать ваш проект на безопасность. Да, возможно, это не закроет сайт от дыр, но сделает его намного безопаснее. Советы он дает достаточно хорошие по этому стеку.

Так вы сможете быстро научиться собирать свои проекты, ускоряясь с каждым разом. И вместо разработки в течение 3-6 месяцев, сможете запускать сайты в течение месяца.

Тут хочу сделать два важных замечания:

1. Никогда не запускайте свой крипто проект без внешнего аудита! Не смотря на то, что нейронки и статические анализаторы уже хороши сегодня, они недостаточны для безопасности! Аудит обязателен!
2. Уделяйте время безопасности и вашему собственному сайту и серверу, где он хранится. Фронтенд и бекенд сайтов проще править в продакшене, чем смарт контракты, но эффект от взлома может быть не меньше!

А вообще, пробуйте, учитесь, разрабатывайте. Объединяйте разные идеи - трейдинг и канбан, переводы крипты и чат в мессенджере, сообщества и сторис, вообще все, что взбредет в голову. Создавайте дизайны. Сейчас это проще, чем было!

#vibe
🔥91
Весь курс по Solidity на одной платформе!

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

Всего, на момент запуска, будет представлено 5 модулей:

- 1 модуль - для начинающих;
- 2 модуль - для продолжающих;
- 3 модуль - низкоуровневые операции и прокси;
- 4 модуль - Foundry;

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

Кроме того, в 3 модуле будут обновлены и добавлены несколько уроков с учетом последних EIP в сети. А в Foundry войдут уроки с более сложными вариантами проведения тестов, в том числе - инвариант тестирование.

Все это будет в удобном формате с заданиями и материалами, которые можно скачать.

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

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

Работа кипит! Держу вас в курсе!

#solidity
🔥29
Книга Mastering Ethereum: 2-е издание

Книга Mastering Ethereum — это один из тех редких ресурсов, которые действительно помогают понять, как работает Ethereum изнутри. Она не ограничивается базовыми примерами или быстрыми инструкциями, а показывает экосистему целиком: от структуры блоков до принципов работы смарт-контрактов. Благодаря этому книга остаётся полезной как новичкам, так и тем, кто уже давно пишет на Solidity.

Одним из самых сильных разделов является описание Ethereum Virtual Machine. Авторы доступно объясняют, что именно происходит при выполнении кода и почему контрактная логика работает так, как работает. Когда начинаешь понимать EVM, многие вещи в Solidity становятся понятнее, а ошибки — менее случайными.

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

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

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

Если вам хочется не просто писать смарт-контракты, а понимать, что происходит на уровне протокола, Mastering Ethereum — один из лучших путеводителей, который стоит добавить в свою библиотеку.

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

#book
👍206
Про поиск работы в Web3

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

Много вопросов было и в чате, и в моих диалогах с восклицаниями, что сейчас трудно найти работу и новички никому не нужны. И в 99% ТАКИЕ новички действительно никому не нужны. Какие такие? Сейчас расскажу.

При первом же вопросе: "А что вы сделали, чтобы найти работу?", половина спрашивающих просто пропадает, еще половина говорит, что просматривали пару сайтов с вакансиями. И эти пара сайтов зачастую: Indeed, LinkedIn и еще пара тройка в первой выдаче гугла. И все. На этом весь поиск работы заканчивается.

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

Для поиска работы нужно показать себя и свои знания. Не достаточно пройти курс обучения и создать свой токен в Ремиксе. Вероятно, это было ОК года 3-4 назад, но не сейчас.

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

Считайте, что нужно быть в трех сферах видимости: GitHub, Twitter/Blog, Public records.

Начнем с GitHub. Это то место, где молодой разработчик может показать свои знания и навыки. Если вы хотите получить работу, то у вас должно быть несколько публичных репо и "необычными проектами". Это могут быть не стандартные пути использования прокси, низкоуровневые паттерны, крутые тесты и т.д. Только ничего самого банального - типа токена ERC20, NFT, клона Unswap или Aave. Простая копипаста или базовый уровень видно сразу и никого не заинтересуют.

Спросите у нейронки, как сделать свой github профиль лучше и выделить его на фоне других. Тот же ChatGPT дает прекрасные советы.

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

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

Все это работа в сообществе.

Без этого никто вас не будет искать. Даже если вы научитесь писать весь код на assembly, и проводить инвариант / fv тесты, то найти работу просто откликаясь на вакансии имеет крайне низкий шанс.

Если вы действительно хотите получить работу в web3, то параллельно с обучением развивайте эти три сферы. Тогда шансы на хорошую должность сильно возрастут.

#job
👍238🔥3💯1
Передел на рынке аудита смарт контрактов

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

На платформе CodeHawks, та которую запускала компания Патрика Колинса, создателя самого популярного англоязычного курса по Solidity, не было вообще никаких конкурсов с марта 2025 года. Другая платформа Hats Finance объявила о своем закрытии.

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

В компании Immunefi, крупнейшей площадке по баг баунти, также проходили недавно сокращения (по сообщениям в Твиттере. Тут не знаю, с чем все связано).

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

При всем при этом развиваются и сторонние программы, которые с помощью ИИ будут искать баги. Чуть ли не каждый месяц я сталкиваюсь с твитами-анонсами таких программ. Да я и сам буду готов в следующем месяце запустить бета-тесты своего mcp сервера...

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

"Золотой век" для аудиторов новичков и середнячков закончен. Уже нельзя заработать хорошие деньги на конкурсах с простыми багами, которые валидировались год-два назад. Сейчас будут зарабатывать только опытные аудиторы и белые хакеры, которые могут разбирать контракты на "атомы" и придумывать такие сценарии атак, которые еще не известны нейросетям.

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

С удовольствием наблюдаю за происходящим!

#audit
🤔6🔥3💯1
Как оформить свой профиль на GitHub?

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

Для тех, кто ещё не знаком с этой возможностью: профиль на GitHub представляет собой обычный репозиторий с названием, совпадающим с вашим никнеймом (например, username/username), содержащий единственный файл — README .md. Благодаря гибкости Markdown и креативному подходу, даже с помощью одного файла можно создать выразительную и профессионально оформленную витрину своих достижений.

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

1. https://github.com/KenanGain
2. https://github.com/daria-stanilevici
3. https://github.com/Andrew6rant
4. https://github.com/thmsgbrt
5. https://github.com/ouuan
6. https://github.com/Shubhamsaboo
7. https://github.com/andyruwruw
8. https://github.com/DenverCoder1
9. https://github.com/MrStanDu33
10. https://github.com/0x3b33

Есть и более простые:

11. https://github.com/0xJuancito
12. https://github.com/Al-Qa-qa

P.S. Вы можете открывать их readme файлы и копировать интересующие вас блоки в свой файл.

А вы уже оформляли свой GitHub-профиль? Делитесь опытом — какие инструменты, подходы или решения вы использовали?

#github
🔥14