Recap: Deposit Front-run Vulnerability Mitigation

Tranchess
4 min readMar 17, 2023

--

On February 24, 2023, the Tranchess team was informed of a potential front-run vulnerability in the EthStakingStrategy contract via Tranchess’ Immunefi Bug Bounty program by an anonymous whitehat (later confirmed to be Jade Han from Kalos Security). The vulnerability can only be exploited by authorized node operators; The potential risk revolves around an authorized node potentially able to transfer out a limited amount of the strategy funds awaiting deposit. On the onset of discovery on Feb. 24, 2023, there were a total of 14 validator keys at risk, evoking a maximum of 14 * 32 = 448 ETH potential loss.

Post discovery, Tranchess had immediately changed the ETH fund cap value to 0, effectively preventing any illicit fund operations, whilst allowing the tech team to execute a mid-term mitigation plan to fix the vulnerability at the same time.

At the time of this recap, the vulnerability had been mitigated. All funds are safe. A bounty of 44.8 ETH (10% of the total potential loss, the maximum payment of our bug bounty program) has been paid via Tranchess treasury to the whitehat. We want to take this opportunity here to thank them again for discovering and reporting the exploit, as well as the ImmuneFi community for their support.

The Vulnerability

The attack vector is fundamentally made possible by the Ethereum Consensus Layer’s design choice of not enforcing the same withdrawal credential for subsequent deposits. A malicious node operator could associate the validator’s public key with the operator-controlled withdrawal credentials by front-running a deposit transaction that assigns the protocol-controlled withdrawal credentials, taking control of the withdrawal and disrupting the proper distribution of the user withdrawals.

The Mitigation

For the mid-term solution, Tranchess rolled out the following new on-chain SafeStaking modules working with off-chain safeguard nodes to secure all future deposits.

Here are the details of the fixing process:

  1. In the EthStakingStrategy module, we have restricted public access to the deposit function to only the SafeStaking module.
  2. In the NodeOperatorRegistry module, we have restricted public access to the updateVerifiedCount function to only the SafeStaking module. And we have added a registryVersion variable to record validator-key-related editions and match them with the offline snapshot.
  3. The SafeStaking module is a new contract that contains two wrappers (safeDeposit and safeVerifyKeys) around the deposit and updateVerifiedCount functions, respectively.
  • The module maintains a new Safeguard role.
  • For the safeDeposit function, we now require off-chain collection of safeguard signatures attesting to a specific snapshot of valid validator states. Any one of the safeguards can trigger an emergency pause when noticing abnormal activities.
  • For the safeVerifyKeys function, we also require off-chain collection of safeguard signatures attesting to a specific set of verified keys. Any one of the safeguards can trigger an emergency pause when noticing abnormal activities.

This fix effectively restrains the attack surface of the aforementioned front-run by requiring a group of trusted off-chain parties to independently attest and collectively enforce the security of the next deposits. The fix places two separate checks on-chain and off-chain for verification before performing the actual deposit. The SafeStaking module will only green-light the deposit and initiate the original deposit logic after all 7 of the criteria below are verified on-chain and true:

  • Verify the Integrity of the Execution Layer:
  1. Observed deposit root matches with on-chain deposit root from the official DepositContract
  2. Observed registry version matches with on-chain registry version from NodeOperatorRegistry
  3. The number of safeguard signatures is greater or equal to safeguard quorum
  4. All signatures are valid, and each signed by a different member of the Safeguard role
  5. Observed blockHash matches with the blockhash(blockNumber)
  • Limit maximum possible amount of funds under risk by a malicious majority of safeguards
  1. The deposit amount does not exceed the ceiling maxDepositAmount
  2. The current timestamp should be at least minDepositTimeInterval away from the last deposit timestamp

We provide further justification for using the deposit root as the certification for no front-run deposit to any pubkey since our off-chain check. Here’s a step-by-step overview to initiate a deposit transaction:

  1. Get the finalized epoch on Beacon Chain, and its corresponding block number N_0 and block hash H_0
  2. Read Beacon Chain balances of all pubkeys at the end of the finalized epoch, making sure there’s no malicious deposit till block H_0
  3. Get the latest block number N_1 and its block hash H_1
  4. Get all Deposit events between block H_0 and H_1, making sure there’s no malicious deposit till block H_1
  5. Read get_deposit_root() in block H_1
  6. Send a deposit transaction to SafeStaking with N_1, H_1 and this get_deposit_root()

Since the return value of get_deposit_root() of the Deposit Contract changes every time a new deposit is made, if there’s any deposit after block N_1 but before any transaction (by either a malicious validator or anyone else), get_deposit_root() returns a different hash, and the transaction reverts. Similarly, if there’s any deposit in a reorganized block N_1, its block hash does not match H_1 our transaction also reverts. As a result, we conclude that as long as the contract sees the same get_deposit_root() as what we saw off-chain, it’s guaranteed that there’s no deposit (to any pubkey) since our off-chain check.

Smart Contract Deploy Plan

  • Deploy EthStakingStrategy and NodeOperatorRegistry
  • Initialize NodeOperatorRegistry (copy node operators and pubkeys from the old NodeOperatorRegistry).
  • Deploy SafeStaking and initialize it (set safeguards and quorum).
  • Deploy BeaconStakingOracle and initialize it (set members and quorum).
  • Connect the smart contracts (set strategy’s reporter to BeaconStakingOracle , set strategy’s safe staking to SafeStaking).
  • Deploy WithdrawalManager .
  • Propose strategy update: FundV4.proposeStrategyUpdate() .
  • Wait for the internal 3-day timelock (hardcoded in FundV4) and execute the following transactions in a TimelockController batch.
  • Transfer ETH from the old strategy to the fund: EthStakingStrategy.transferToFund() .
  • Apply strategy update: FundV4.applyStrategyUpdate() .
  • Update all WithdrawalManager contracts: WithdrawalManagerFactory.updateImplementation() .
  • Initialize the new EthStakingStrategy (copy the last beacon balance report from the old EthStakingStrategy).
  • Re-open ETH staking: PrimaryMarketV4.updateFundCap() .

Long-term Solution

Tranchess plans to establish a committee and upgrade the smart contracts for auto-checks and monitoring of the funds and deposits. It will require significant work, and we will update the solution in future releases and proposals with our community.

--

--