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:
- In the
EthStakingStrategy
module, we have restricted public access to thedeposit
function to only theSafeStaking
module. - In the
NodeOperatorRegistry
module, we have restricted public access to theupdateVerifiedCount
function to only theSafeStaking
module. And we have added aregistryVersion
variable to record validator-key-related editions and match them with the offline snapshot. - The
SafeStaking
module is a new contract that contains two wrappers (safeDeposit
andsafeVerifyKeys
) around thedeposit
andupdateVerifiedCount
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:
- Observed deposit root matches with on-chain deposit root from the official DepositContract
- Observed registry version matches with on-chain registry version from
NodeOperatorRegistry
- The number of safeguard signatures is greater or equal to safeguard quorum
- All signatures are valid, and each signed by a different member of the Safeguard role
- Observed blockHash matches with the blockhash(blockNumber)
- Limit maximum possible amount of funds under risk by a malicious majority of safeguards
- The deposit amount does not exceed the ceiling
maxDepositAmount
- 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:
- Get the finalized epoch on Beacon Chain, and its corresponding block number N_0 and block hash H_0
- 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
- Get the latest block number N_1 and its block hash H_1
- Get all Deposit events between block H_0 and H_1, making sure there’s no malicious deposit till block H_1
- Read
get_deposit_root()
in block H_1 - Send a deposit transaction to
SafeStaking
with N_1, H_1 and thisget_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
andNodeOperatorRegistry
- Initialize
NodeOperatorRegistry
(copy node operators and pubkeys from the oldNodeOperatorRegistry
). - 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 toSafeStaking
). - Deploy
WithdrawalManager
. - Propose strategy update:
FundV4.proposeStrategyUpdate()
. - Wait for the internal 3-day timelock (hardcoded in
FundV4
) and execute the following transactions in aTimelockController
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 oldEthStakingStrategy
). - 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.