Как потерять 13 долларов средств пользователей (как разработчик блокчейна)

Как потерять 13 долларов средств пользователей (как разработчик блокчейна)

20 декабря 2022 г.

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

Несмотря на то, что криптовалюты и TradFi пострадали больше всего, многие компании все еще создают свои токены, протоколы и продукты DeFi. Вы один из них?

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

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

Современные языки, такие как JavaScript и Python, используют «утиную типизацию» для определения типа переменной. Если мы присвоим такой формуле a = 2 + 2 переменной, интерпретатор языка узнает, что имеет дело с числами, и будет выполнять математические операции над этим литералом.

Утиное типирование можно объяснить следующим предложением: «Если оно ходит как утка и крякает как утка, то это должна быть утка». Если присмотреться к его смыслу, то он обретает смысл. Если литерал состоит из букв и цифр, он должен быть строкой, и это понятно. Но что, если у него есть числа?

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

<цитата>

"Если он ходит как утка и крякает как утка, значит, это утка", верно? На самом деле это не так.

Кратко о номиналах Эфириума

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

Наименьший номинал равен 1 вей, а 1 эфир равен 1 000 000 000 000 000 000 вей (18 нулей). Повторяю - 18 нули. Нам трудно уложить в голове такие большие числа, но они имеют значение и имеют большое значение.

Следующим распространенным номиналом является 1 gwei. 1 эфир равен 1 000 000 000 gwei (9 нулей). Gwei более терпима для людей — в конце концов, все хотят стать миллионерами, верно? ? (подмигнуть, подмигнуть)

Подытожим - 1 эфир равен:

  • 1,000,000,000 gwei (9 zeros)
  • 1,000,000,000,000,000,000 wei (18 zeros)

Техническое примечание: Ethereum имеет два уровня — уровень исполнения и уровень консенсуса. Уровень выполнения использует wei для представления значений эфира, а уровень консенсуса использует gwei. Если вы разработчик блокчейна, вам нужно научиться взаимодействовать с обоими.

Пример из реальной жизни: подсказка Stakefish’ и пул MEV

Я работаю инженером-программистом в stakefish. Я отвечаю за создание нашей палитры продуктов DeFi, и одним из последних является наш совет и пул MEV для Ethereum.

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

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

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

Как я уже упоминал, у нас есть смарт-контракт, который получает советы по транзакциям и вознаграждения MEV, заработанные валидаторами. Это означает, что наш смарт-контракт имеет довольно большой баланс. Сейчас это 963+ эфира ($1,1 млн), и у нас есть 8671 валидатор.

Критической частью, отвечающей за синхронизацию между выполнением Ethereum и уровнем консенсуса, является Oracle. Это очень важная система, которая позволяет нам определить, какие валидаторы вносят свой вклад в пул.

Оракул написан на Python, но его можно было бы написать и на JavaScript — проблема остается неизменной, и я скоро это докажу.

Давайте углубимся в код!

Почему важны типы данных

Баланс смарт-контракта сейчас составляет 963 135 554 442 603 402 422 wei (963 эфира). Это число трудно понять не только людям, но и компьютерам (точнее, языковым переводчикам). Давайте проверим JavaScript:

const vault_balance = parseInt("963135554442603402422")
console.log(vault_balance) 
// 963135554442603400000 (lost 2422 wei in total)

Я только преобразовал баланс из string в int, и у меня уже 2422 wei короткий. Мы еще не выполнили ни одного уравнения.

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

const vault_balance = parseInt("963135554442603402422")
const validator_count = 8671

const avg_validator_contribution = vault_balance / validator_count
// 111075487768723730 (lost 7 wei per validator)

Средняя доля составляет 0,111 эфира. Но эта сумма неверна - нам на самом деле не хватает 7 вэй. Всего 60 697 wei (7 wei умножить на 8671 валидатор). Я покажу правильный номер позже.

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

И я возьму в качестве примера один случайный валидатор, внесший вклад в смарт-контракт, баланс которого составляет 32,779 эфира.

const vault_balance = parseInt("963135554442603402422") // (lost 2422 wei)
const validator_count = 8671
const avg_validator_contribution = vault_balance / validator_count // (lost 7 wei)

const initial_deposit = parseInt("32000000000000000000")
const validator_balance = parseInt("32779333896000000000")

const total_validator_rewards = validator_balance - initial_deposit + avg_validator_contribution
// 890409383768723700 (lost 23 wei per validator)

Общее вознаграждение, полученное этим валидатором, равно 0,8904 эфира, но это значение также не является точным. На данный момент мы неправильно посчитали 199 443 wei (23 wei умножить на 8671 валидатор). Как видите, такой способ подсчета чисел не является устойчивым.

Что пошло не так?

С кодом выше есть две проблемы:

* В JavaScript максимальное безопасное значение для целых чисел равно 2^53 - 1. Это означает, что он может обрабатывать до 9007199254740991 wei (0,009 эфира)

* Технически мы могли бы использовать BigInt, но у нас были бы проблемы с делением. В итоге мы получим «плавающие» значения. Поплавки — корень всех зол в финансах, потому что они приблизительны. Это означает, что они теряют точность. Нам нужно использовать десятичные дроби. (Основное различие между десятичным числом и числом с плавающей запятой заключается в том, что десятичное значение хранит точное значение, а число с плавающей запятой приблизительное.)

Если вы когда-либо занимались программированием на JavaScript, связанным с Ethereum, вы наверняка слышали о ethers.js. Эта библиотека содержит все необходимые утилиты для взаимодействия с блокчейном. Чтобы решить описанную выше проблему, мы воспользуемся одним из инструментов под названием BigNumber, который поддерживает очень большие числа и правильно обрабатывает десятичные дроби.

Давайте сделаем это!

const vault_balance = BigNumber.from("963135554442603402422") // no loss
const validator_count = BigNumber.from(8671)
const avg_validator_contribution = vault_balance.div(validator_count) // no loss
// 111075487768723723

const initial_deposit = BigNumber.from("32000000000000000000")
const validator_balance = BigNumber.from("32779333896000000000")

const total_validator_rewards = validator_balance.sub(initial_deposit).add(avg_validator_contribution)
// 890409383768723723

Как видите, теперь мы получили точное число. Как я узнаю, что это действительно правильный номер? Я повторю то же упражнение на Python, чтобы доказать свою правоту.

Попробуем на Python

Python поддерживает длинные целые числа, поэтому значения не будут внезапно обрезаны, как мы видели в JavaScript. К сожалению, он по-прежнему определяет все числа с плавающей запятой как float по умолчанию:

vault_balance = int("963135554442603402422") # no loss
validator_count = 8671
avg_validator_contribution = vault_balance / validator_count
# 111075487768723728 (5 wei too much)

initial_deposit = int("32000000000000000000")
validator_balance = int("32779333896000000000")

total_validator_rewards = validator_balance - initial_deposit + avg_validator_contribution
# 890409383768723712 (lost 11 wei)

Интересно, где именно он потерял точность? Подразделение преобразовало avg_validator_contribution в float вместо decimal. Правильный фрагмент будет выглядеть так:

vault_balance = Decimal("963135554442603402422")
validator_count = Decimal(8671)
avg_validator_contribution = vault_balance / validator_count
# 111075487768723723

initial_deposit = Decimal("32000000000000000000")
validator_balance = Decimal("32779333896000000000")

total_validator_rewards = validator_balance - initial_deposit + avg_validator_contribution
# 890409383768723723

Теперь значения, возвращаемые Python и JavaScript, являются точными. Проверьте сами!

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

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


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


Оригинал
PREVIOUS ARTICLE
NEXT ARTICLE