Analysis of a possible attempt at a (failed) exploit

There was a malicious proposal. Withdraw your locked funds from the Governance contract.

Disclaimer: There was no TORN outflows. Funds safu for now.

From the time of writing, approximately 10 hours and 40 minutes ago, an attacker executed a transaction which was a possible exploit attempt of reward rate calculations (for TORN locking).

The attempt consisted of deploying a contract at the address 0x03eCF0d22f9cCd21144a7d492cf63b471916497A and calling the function with the signature c3375ca5 which iteratively deployed contract, and for each called a function with the signature 93d3a7b6 twice.

The first function call to deployed contract, calling the function 93d3a7b6, served to approve transfers towards Governance from the deployed contract, and the second (same) function call served to lock tokens in Governance which would trigger reward updates.

In all cases 0 TORN was approved and locked.

This was a possible attempt at exploiting the following code sequence from the GovernanceStakingUpgrade and the TornadoStakingRewards contracts:

// From GovernanceStakingUpgrade.sol
modifier updateRewards(address account) {
  try Staking.updateRewardsOnLockedBalanceChange(account, lockedBalance[account]) {
    emit RewardUpdateSuccessful(account);
  } catch (bytes memory errorData) {
    emit RewardUpdateFailed(account, errorData);
  }
  _;
}
function lockWithApproval(uint256 amount) public virtual override updateRewards(msg.sender) {
    super.lockWithApproval(amount);
 }

// From TornadoStakingRewards.sol
function updateRewardsOnLockedBalanceChange(address account, uint256 amountLockedBeforehand) external onlyGovernance {
  uint256 claimed = _updateReward(account, amountLockedBeforehand);
  accumulatedRewards[account] = accumulatedRewards[account].add(claimed);
 }
function _updateReward(address account, uint256 amountLockedBeforehand) private returns (uint256 claimed) {
  if (amountLockedBeforehand != 0)
    claimed = (accumulatedRewardPerTorn.sub(accumulatedRewardRateOnLastUpdate[account])).mul(amountLockedBeforehand).div(
      ratioConstant
    );
  accumulatedRewardRateOnLastUpdate[account] = accumulatedRewardPerTorn;
  emit RewardsUpdated(account, claimed);
}

There may be the possibility that this was a test run and that the contracts will be later loaded with TORN due to the approval events. But it would also be possible to execute this logic instantly, as it was done, so the infinite approvals possibly do not make sense, beyond maybe preparing a larger sybil attack through a lower gas limit on next calls.

If the deployed contracts would be loaded with TORN, each would lock some amount of TORN into governance and update their reward state, but initially no funds would be distributed. If called again say with a 0 value after locking, the formerly used balance would be used and rewards would (properly) be allocated.

So currently it is not clear what the intention with this was beyond a failed attempt. The structure does indicate that the attacker points towards there being a sybil vulnerability.

2 Likes