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

В протоколе PoolTogather заметил простой и удобный способ переключения prank вызовов внутри одной функции.

К примеру, у нас есть такая функция:

solidity
function someTestCall() public {

   // действия от пользователя_1

   // переключение на пользователя_2
   // выполнение некоторых действий

   // обратное переключение на пользователя_1
   // продолжение действий

}


Да, можно все это сделать с обычными prank, stop/startPrank, hoax, но в итоге может получиться ситуация, когда в тесте будет слишком много этих не нужных читкодов.

Вот как сделала команда PoolTogather.

Они создали модификатор:


solidity
    modifier prankception(address prankee) {
        address prankBefore = currentPrankee;
        vm.stopPrank();
        vm.startPrank(prankee);
        _;
        vm.stopPrank();
        if (prankBefore != address(0)) {
            vm.startPrank(prankBefore);
        }
    }


а затем поместили его в нужные служебные функции, например, в такую:

solidity
    function _accrueYield() internal virtual override prankception(_aTokenWhale) {
        IERC20(_aToken).transfer(_yieldVault, 10 ** assetDecimals);
        vm.warp(block.timestamp + 1 days);
    }


и теперь ее можно вызывать внутри других функций, с изменение роли вызывающего:

solidity
    function accrueYield() public returns (uint256) {
        uint256 assetsBefore = prizeVault.totalAssets();

@>        _accrueYield();

        uint256 assetsAfter = prizeVault.totalAssets();
        if (yieldVault.balanceOf(address(prizeVault)) > 0) {
            require(assetsAfter > assetsBefore, "yield did not accrue");
        } else {
            if (underlyingAsset.balanceOf(address(prizeVault)) > 0) {
               ...
            } else {
                ...
            }
        }
        return assetsAfter - assetsBefore;
    }


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

На мой взгляд достаточно чистый и красивый код теста.

#foundry #prank
👍5🔥1
Как работает TWAP в Uniswap V2. Часть 1

С сегодняшнего дня начнем говорить о том, как работает оракул TWAP в версии V2.

Для начала вспомним, а что такое "цена" за актив в пуле вообще.
Например, у нас есть пул, в котором 1 Эфир и 2000 USDT. Цена токена рассчитывается по следующей формуле:

price(token_0) = balance(token_1) / balance(token_0);

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

Игнорируя decimals, из нашего примера мы можем сказать, что 1 USDT будет равен 0.0005 Эфира.

P.S. Uniswap использует число с фиксированной точкой с точностью 112 бит с каждой стороны десятичной дроби, что занимает в общей сложности 224 бита, и при упаковке с 32-битным числом оно занимает один слот.

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

Хакеры и другие вредоносные пользователи (мне нравится выражение на английском языке malicious user), могут использовать такие инструменты DeFi протоколов, как флеш займы, чтобы манипулировать текущей ценой в пуле, из-за чего получаемые данные в нашем контракте от Оракула станут недостоверными.

Оракулы Юнисвап V2 защищаются от этого следующими способами:

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

2. Также в расчет оракула не включается текущий баланс;

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

Так как же работает TWAP?

TWAP (Time Weighted Average Price) похож на простую скользящую среднюю, за исключением того, что то время, когда цена оставалась неизменной дольше, получает больший вес. TWAP как бы предает больший вес цене по тому, как долго она остается на определенном уровне. Например,

1. За последние сутки цена актива составляла $10 в течение первых 12 часов и $11 в течение вторых 12 часов. Средняя цена такая же, как и средневзвешенная по времени: $10,5.

2. За последние сутки цена актива составляла $10 за первые 23 часа и $11 за последние. Ожидаемая средняя цена должна быть ближе к $10, чем к $11, но она все равно будет находиться между этими значениями. Точнее, она будет равна ($10 * 23 + $11 * 1) / 24 = $10,0417.

3. За последние сутки цена актива составляла $10 в течение первого часа и $11 в течение последних 23 часов. Мы ожидаем, что TWAP будет ближе к $11, чем к 10. Точнее, он будет равен ($10 * 1 + $11 * 23) / 24 = $10,9583

Дальше узнает, как это все работает в коде контракта.

#uniswap #v2 #twap
👍5🔥21👌1
Как работает TWAP в Uniswap V2. Часть 2

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

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

Решение Uniswap заключается в том, что он хранит только числитель значений - каждый раз, когда происходит изменение коэффициента ликвидности (вызывается mint, burn, swap или sync), он записывает новую цену и то, как долго держалась предыдущая цена.

Переменные price0Cumulativelast и price1CumulativeLast на скрине выше являются общедоступными, поэтому заинтересованные пользователи сами должны делать snapshot при необходимости.

Единственное, что нужно отметить это: price0CumulativeLast и price1CumulativeLast обновляются только на строках 79 и 80 в коде выше (оранжевый круг), и могут только увеличиваться, пока не переполнятся (overflow). Нет никакого механизма, заставляющего их "уменьшаться". Они всегда увеличиваются при каждом вызове _update, который и происходит после swap, mint или burn. Это означает, что эти переменные только увеличивают цены с момента запуска пула.

#uniswap #v2 #twap
3🔥1👌1
Как работает TWAP в Uniswap V2. Часть 3

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

Цена A по отношению к B - это просто A/B и наоборот. Например, если у нас есть 2000 USDC в пуле (без учета десятичных дробей) и 1 Ether, то цена 1 Ether равна просто 2000 USDC / 1 ETH.

Таким образом, цена USDC, выраженная в ETH, - это просто это число с перевернутыми числителем и знаменателем.

Однако мы не можем просто "перевернуть" одну из цен, чтобы получить другую, когда мы накапливаем цены (не уверен в точном переводе accumulating pricing в данном случае).

Рассмотрим следующее. Если наш накопитель цен начинается с 2 и прибавляет 3, мы не можем просто сделать так:

1 / 2+3 != 1/2 + 1/3

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

Если Eth в 1 000 раз "ценнее", чем USDC, то USDC в 1 000 раз "менее ценен", чем USDC. И чтобы хранить эти значения более точно, число с фиксированной точкой должно иметь одинаковый размер по обе стороны decimals, поэтому Uniswap выбрал u112x112 (объяснение в первом посте).

Далее пара слов о значении PriceCumulativeLast, которое всегда увеличивается, пока не переполнится, а затем снова продолжает рост.

Uniswap V2 был создан до версии Solidity 0.8.0, поэтому арифметика по умолчанию могла привести к overflow/underflow. Корректные современные реализации ценового оракула должны использовать блок unchecked, чтобы все работало как положено.

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

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

Мы фиксируем priceAccumulator на 80, а через несколько транзакций/блоков priceAccumulator поднимается до 110, но переполняется до 10. Мы вычитаем 80 из 10, что дает -70. Но значение хранится как беззнаковое целое, поэтому оно дает -70 mod(100), что равно 30. Это тот же результат, который мы ожидали бы, если бы переполнения не было (110-80=30).

Это справедливо для всех границ переполнения, а не только для 100 в нашем примере.

То же самое произойдет, если мы переполним временную метку. Поскольку для ее представления мы используем uint32, отрицательных чисел не будет. Опять же, предположим, что для простоты мы переполнимся на 100. Если мы делаем снимок в момент времени 98 и обращаемся к оракулу цен в момент времени 4, то прошло 6 секунд. 4 - 98 % 100 = 6, как и ожидалось.

Теперь вы знаете о TWAP чуточку больше. Далее мы продолжим говорить о Uniswap V2, V3 и других DeFi протоколах.

#uniswap #v2 #twap
🔥4👌2
Uniswap V2: UniswapV2Library

Мы продолжаем разбирать код одного из самых опулярны протоколов, и следующие пару дней посвятим библиотеке UniswapV2Library:

https://github.com/Uniswap/v2-periphery/blob/master/contracts/libraries/UniswapV2Library.sol

Библиотека Uniswap V2 упрощает некоторые взаимодействия с контрактами пар токенов и активно используется контрактами Router. Она содержит всего восемь функций.

getAmountOut() и getAmountIn()

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

amount_out = reserve_out * amount_in / reserve_in + amount_in

Учитывая это, функция getAmountOut() в UniswapV2Library.sol должна быть понятна. Обратите внимание, что числа масштабируются на 1000, чтобы учесть комиссию в 0,3%.

    function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) internal pure returns (uint amountOut) {
require(amountIn > 0, 'UniswapV2Library: INSUFFICIENT_INPUT_AMOUNT');
require(reserveIn > 0 && reserveOut > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY');
uint amountInWithFee = amountIn.mul(997);
uint numerator = amountInWithFee.mul(reserveOut);
uint denominator = reserveIn.mul(1000).add(amountInWithFee);
amountOut = numerator / denominator;
}


Связка логики между getAmountOut() и getAmountsOut() при проведении операций между различными парами токенов

Если трейдер предоставляет последовательность пар (A, B), (B, C), (C, D) и циклически вызывает getAmountOut, начиная с определенной суммы A, то количество полученного токена D может быть предсказано.

Адрес контракта пары UniswapV2 для каждого (A, B), (B, C) и т. д. детерминировано выводится из адресов токенов и адреса фабрики, развернувшей пару с помощью функции create2. Если даны два токена (A, B) и адрес фабрики, функция pairFor() выводит адрес контракта парного пула UniswapV2 для этой пары, используя в качестве вспомогательной функции sortTokens().

    function pairFor(address factory, address tokenA, address tokenB) internal pure returns (address pair) {
(address token0, address token1) = sortTokens(tokenA, tokenB);
pair = address(uint(keccak256(abi.encodePacked(
hex'ff',
factory,
keccak256(abi.encodePacked(token0, token1)),
hex'96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f'
))));
}


Теперь, когда мы знаем адреса всех пар, мы можем получить резервы каждой из них и предсказать, сколько токенов мы получим в конце цепочки обменов. Ниже приведен код функции getAmountsOut (обратите внимание, что написано"Amounts", а не на "Amount").

    function getAmountsOut(address factory, uint amountIn, address[] memory path) internal view returns (uint[] memory amounts) {
require(path.length >= 2, 'UniswapV2Library: INVALID_PATH');
amounts = new uint[](path.length);
amounts[0] = amountIn;
for (uint i; i < path.length - 1; i++) {
(uint reserveIn, uint reserveOut) = getReserves(factory, path[i], path[i + 1]);
amounts[i + 1] = getAmountOut(amounts[i], reserveIn, reserveOut);
}
}


Обратите внимание на пару моментов:

1. Смарт-контракт не может самостоятельно определить оптимальную последовательность пар, ему нужно сообщить список пар для расчета цепочки свопов. Это лучше всего делать вне цепочки.

2. Он возвращает не просто конечную сумму токеновOut в цепочке, а сумму на каждом шаге.

Далее поговорим об остальных функциях этой библиотеки.

#uniswap #v2 #UniswapV2Library
🔥3👍1
Uniswap V2: UniswapV2Library. Часть 2

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

getReserves()

Функция getReserves - это просто обертка функции getReserves из контракта пула токенов Uniswap V2, за исключением того, что она также удаляет временную метку последнего обновления цены.


//Library

function getReserves(address factory, address tokenA, address tokenB) internal view returns (uint reserveA, uint reserveB) {
(address token0,) = sortTokens(tokenA, tokenB);
(uint reserve0, uint reserve1,) = IUniswapV2Pair(pairFor(factory, tokenA, tokenB)).getReserves();
(reserveA, reserveB) = tokenA == token0 ? (reserve0, reserve1) : (reserve1, reserve0);
}

//pool

function getReserves() public view returns (uint112 _reserve0, uint112 _reserve1, uint32 _blockTimestampLast) {
_reserve0 = reserve0;
_reserve1 = reserve1;
_blockTimestampLast = blockTimestampLast;
}


quote()

Напомним, что цена актива определяется по следующей формуле:

price(foo) = reserves(bar) / reserves(foo)

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


function quote(uint amountA, uint reserveA, uint reserveB) internal pure returns (uint amountB) {
require(amountA > 0, 'UniswapV2Library: INSUFFICIENT_AMOUNT');
require(reserveA > 0 && reserveB > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY');
amountB = amountA.mul(reserveB) / reserveA;
}


Если вы хотите узнать, сколько нужно вложить или ожидать от сделки, а также последовательность сделок по парам, UniswapV2Library - это инструмент, который следует использовать.

К слову сказать, если хотите потренироваться в работе с поиском уязвимостей, то Damn Vulnerable Defi предлагает прекрасную задачу по этой теме:

https://www.damnvulnerabledefi.xyz/challenges/puppet-v2/

Далее мы будем разбирать контракты Router.

#uniswap #v2 #UniswapV2Library
👍3
Сборник ресурсов по Uniswap V2/V3

Недавно в Твиттере кто-то поделился прекрасным репо, где собраны различные материалы по работе с двумя версиями Uniswap - V2 и V3.

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

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

Ссылка на сборник: https://github.com/Sabnock01/uniswap-resources

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

#uniswap #github
🔥5
Разбор Uniswap V2: Router контракты

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

Итак, этот контракт отвечает за:

1. Безопасный майнинг и сжигание токенов LP (добавление и удаление ликвидности)

2. Безопасный своп токенов;

3. Возможный обмен Эфира при помощи его ERC-20 версии - WETH;

4. Проверки проскальзывания при работе с основным контрактом;

5 Поддержка fee on transfer токенов;

Вообще, если мы зайдем по ссылке репо, то увидим, что там два контакта Router:

UniswapV2Router01.sol
UniswapV2Router02.sol


Вот второй, как раз и добавляет поддержку fee on transfer токенов. Если мы заглянем в него, то увидим, что он наследует от первого контракта и добавляет уникальные функции, например:

function removeLiquidityETHSupportingFeeOnTransferTokens(...) ) public virtual override ensure(deadline) returns (uint amountETH) {}

Теперь о самих функциях.


swapExactTokensForTokens и swapTokensForExactTokens

Обе эти функции используются при свапе токенов, с одной разницей:

1. В swapExactTokensForTokens означает, что количество входного токена, который вы обмениваете, фиксировано.

2. В swapTokensForExactTokens - количество выходного токена, который вы получите, фиксировано.

Если пользователь обменивает только два токена, то он передаст этим функциям массив address[] calldata path [address(tokenIn), address(tokenOut)]. Если пользователь обменивается между пулами, то он укажет [address(tokenIn), address(intermediateToken), ..., address(tokenOut)].


swapExactTokensForTokens

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

Например, предположим, что мы хотим обменять 25 токенов_0 на 50 токенов_1. Если это точная цена в текущем состоянии, это не оставляет никаких шансов на то, что цена изменится до подтверждения нашей транзакции, что приведет к реверту. Поэтому вместо этого мы указываем минимальную цену 49,5 токена_1, неявно оставляя проскальзывание в 1 %.


swapTokensForExactTokens

В этом случае мы указываем, что нам нужно ровно 50 токенов_1, но мы готовы обменять до 25,5 токенов_0, чтобы получить их.


Какую функцию использовать?


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

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


Как работает своп токенов


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

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

Затем обе функции передают токены пользователя в контракт пары (мы же помним, что Uniswap V2 Pair требует, чтобы токены были отправлены на сам контракт до вызова функции swap).

Наконец, они вызывают внутреннюю функцию _swap, о которой пойдет речь далее.

#uniswap #router
1
Разбор Uniswap V2: Router контракты. Часть 2

Продолжаем говорить о контрактах Router и сегодня обратим внимание на _swap(), addLiquidity() и addLiquidityEth().

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

_addLiquidity

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

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

_addLiquidity возьмет суммуADesired и вычислит правильное количество токенов_В, которое будет соответствовать соотношению. Если это количество больше, чем amountBDesired (количество B, которое отправил поставщик ликвидности), то он начнет с amountBDesired и вычислит оптимальное количество B. Логика показана на скрине.

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

Например, предположим, что текущий баланс пары составляет 100 token_0 и 300 token_1. Мы хотим добавить 20 и 60 токен_0 и токен_1 соответственно, но соотношение пар может измениться.

Поэтому вместо этого мы одобряем маршрутизатор на 21 токен_0 и 63 токена_1, указывая при этом, что минимальное количество, которое мы хотим внести, составляет 20 и 60. Если соотношение изменится так, что оптимальное количество токенов0 для депозита составит 19,9, то транзакция вернется обратно.

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

addLiquidity() и addLiquidityEth()

Эти функции не требуют пояснений. Сначала они рассчитывают оптимальное соотношение с помощью _addLiquidity, описанного выше, затем переводят активы в контракт пары, после чего вызывают майнинг. Единственное отличие заключается в том, что функция addLiquidityEth сначала обернет Ether в ETH.

В следующем посте поговорим об удалении ликвидности, fee-on-transfer токенах и библиотеке UniswapV2Library.

#uniswap #router
1
Разбор Uniswap V2: Router контракты. Часть 3

Ну, и заключительная часть разбора Router контракта начинается с функций removeLiquidityEth и removeLiquidity.

Функции удаления ликвидности вызывают burn, и использует параметры amountAMin и amountBMin в качестве проверки безопасности, чтобы гарантировать, чтобы пользователь получил обратно то количество токенов, которое ожидает.

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

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

removeLiquidityWithPermit() и removeLiquidityETHWithPermit()

В строке 109 в приведенном выше файле с серым комментарием send liquidity to pair предполагает, что контракт пары имеет разрешение на передачу токенов LP от поставщика ликвидности для их сжигания. Это означает, что для сжигания LP-токенов требуется сначала одобрить пару. Этот шаг можно пропустить с помощью функции permit().

Router02 и поддержка fee-on-transfer токенов

Чтобы справиться подобными токенами, контракт Router не может напрямую производить расчеты по таким аргументам, как amountIn (для свопа) или liquidity (для удаления ликвидности). В этом случае, добавление ликвидности не будет зависеть от комиссии за перевод токенов, поскольку пользователю начисляется только то, что он фактически переводит в пару.

Контракты Router обеспечивают пользовательский механизм обмена токенов с защитой от проскальзывания, в том числе и между несколькими пулами, а также добавляют поддержку торговли ETH и fee on transfer токенами (в Router02).

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

Кроме того, включена поддержка функций ERC20 Permit.

#uniswap #router
🔥3
Работа с суммами в Foundry

В Твиттере наткнулся на небольшой интересный пост про работу с суммами токенов и их отображению в экспоненциальной форме.

Думаю, в следующих тестах немного поэкспериментировать с этим.

#foundry #ammounts
🔥4
Обновление Dencun

Все никак сам не мог дойти, чтобы изучить более подробно последнее обновление Эфира - Dencun.

К счастью, сегодня в 19:00 по мск на Ютуб канале Ильи будет стрим по этой теме.

Крайне рекомендую всем подключиться!

Ссылка на стрим: https://www.youtube.com/watch?v=mugTyD70rDk

Встретимся вечером!

#dencun
👍3🔥2
Хотите перезапуск курса с нуля?

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

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

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

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

Я хотел бы узнать, сколько участников канала хотели бы зайти на 1 модуль курса? Также напомню, что курс рассчитан на тех, кто ни разу не имел дело с программированием.

Прошу пройти опрос ниже.

#курс
👍141