Перейти к основному содержанию
Этот пример вкратце показывает, как Tenderly можно использовать для анализа и даже предотвращения взломов, подобных случившемуся с Cover Protocol. Cover Protocol был взломан из-за неспособности кода обновлять кеш в storage. Этого потенциально можно было бы избежать, настроив алерты на важные функции, такие как deposit/withdrawal и определённые изменения состояния, чтобы предотвратить атаку. Чтобы вручную проанализировать взлом, нам нужно импортировать смарт-контракт Blacksmith, в котором была обнаружена уязвимость. Если вы знаете точное имя затронутого протокола или уязвимого контракта, можно также искать его напрямую.
Обратите внимание: искать можно только контракты, публично верифицированные в Tenderly или Etherscan. Подробнее о том, как верифицировать контракт в Tenderly, читайте здесь.
Если контракт публично не верифицирован, вы можете сделать это самостоятельно, чтобы иметь возможность использовать его с Tenderly. Если проект — open-source (как Cover), можно перейти в GitHub-репозиторий проекта, найти нужный контракт (и его исходный код) и верифицировать его самостоятельно. Теперь, когда у нас есть контракт, который нужно импортировать в Tenderly, перейдите в свой Dashboard и кликните вкладку Contracts слева, а затем кнопку Add Contract в правом верхнем углу: Вы можете загрузить и верифицировать код контракта либо через Dashboard, либо через CLI [подробнее здесь]: Теперь, после импорта и верификации контракта, давайте проанализируем сам взлом. Мы знаем, что баг был в кеше функции deposit, поэтому симулируем эту функцию. Как видно на скриншоте ниже, после выполнения она завершилась с ошибкой Blacksmith: pool does not exist. Теперь откроем эту ошибку в Tenderly Debugger. На строке 118 мы видим Pool memory pool = pools[_lpToken]; в функции deposit, где данные кешируются, а позже, на строке 125, обновляются — там мы видим _claimCoverRewards(pool, miner); Контракт использовал закешированные данные, потому что сами данные оставались в памяти; поэтому контракт позже рассчитывал функцию, используя эти данные. Кеш не обновлялся, что и привело к этому взлому:
example.sol
miner.rewardWriteoff = miner.amount.mul(pool.accRewardsPerToken).div(CAL_MULTIPLIER);
example.sol
miner.bonusWriteoff = miner.amount.mul(bonusToken.accBonusPerToken).div(CAL_MULTIPLIER);
example.sol
function deposit(address _lpToken, uint256 _amount) external override {
    require(block.timestamp >= START_TIME, "Blacksmith: not started");
    require(_amount > 0, "Blacksmith: amount is 0");

    Pool memory pool = pools[_lpToken];

    require(pool.lastUpdatedAt > 0, "Blacksmith: pool does not exists");
    require(IERC20(_lpToken).balanceOf(msg.sender) >= _amount, "Blacksmith: insufficient balance");

    updatePool(_lpToken);

    Miner storage miner = miners[_lpToken][msg.sender];
    BonusToken memory bonusToken = bonusTokens[_lpToken];

    _claimCoverRewards(pool, miner);
    _claimBonus(bonusToken, miner);

    miner.amount = miner.amount.add(_amount);

    // update writeoff to match current acc rewards/bonus per token
    miner.rewardWriteoff = miner.amount.mul(pool.accRewardsPerToken).div(CAL_MULTIPLIER);
    miner.bonusWriteoff = miner.amount.mul(bonusToken.accBonusPerToken).div(CAL_MULTIPLIER);

    IERC20(_lpToken).safeTransferFrom(msg.sender, address(this), _amount);
    emit Deposit(msg.sender, _lpToken, _amount);
}

  • Новый пул был одобрен для liquidity mining буквально за несколько часов до взлома. Этот пул был абсолютно нормальным, но, поскольку он был новым, в контракте blacksmith не было LP-токенов этого пула.
  • Атакующий внёс некоторое количество токенов этого пула в контракт Blacksmith.
  • Контракт Blacksmith ведёт учёт наград на попер-токен-основе. Если заблокировано много токенов, награда на токен будет мала. Если заблокировано мало токенов, награда на токен будет велика. Соответствующая переменная называется accRewardsPerToken и вычисляется как totalPoolRewards / totalTokenBalance.
  • Затем атакующий вывел почти все LP-токены из контракта Blacksmith, уменьшив значение totalTokenBalance практически до нуля.
  • Затем атакующий снова внёс некоторое количество токенов этого пула в контракт Blacksmith. Именно здесь баг проявил себя во всей красе. Поскольку totalTokenBalance в предыдущей транзакции сильно уменьшился, вновь рассчитанный accRewardsPerToken подскочил вверх. Контракт использует rewardWriteoff, чтобы держать под контролем эффект accRewardsPerToken. Однако из-за бага при расчёте значения rewardWriteoff использовалось старое (маленькое) значение accRewardsPerToken. Из-за этого большое значение accRewardsPerToken осталось без контроля.
  • Затем атакующий вывел свои награды. Поскольку в accRewardsPerToken было большое неконтролируемое значение, общая награда, выплаченная системой, оказалась завышенной, и в итоге контракт заминтил 40 796 131 214 802 500 000 токенов COVER.
Спасибо Mudit Gupta за последнюю часть разбора этого взлома — подробнее об этом в его блоге :)