Frax Ether Liquid Staking contest - parashar's results

A liquid ETH staking derivative designed to uniquely leverage the Frax Finance ecosystem.

General Information

Platform: Code4rena

Start Date: 22/09/2022

Pot Size: $30,000 USDC

Total HM: 12

Participants: 133

Period: 3 days

Judge: 0xean

Total Solo HM: 2

Id: 165

League: ETH

Frax Finance

Findings Distribution

Researcher Performance

Rank: 1/133

Findings: 2

Award: $7,217.00

🌟 Selected for report: 1

🚀 Solo Findings: 1

Findings Information

🌟 Selected for report: parashar

Labels

bug
3 (High Risk)
sponsor acknowledged
sponsor confirmed

Awards

7188.992 USDC - $7,188.99

External Links

Lines of code

https://github.com/code-423n4/2022-09-frax/blob/main/src/frxETHMinter.sol#L120

Vulnerability details

Impact

Frontrunning by malicious validator changing withdrawal credentials

Proof of Concept

A malicious validator can frontrun depositEther transaction for its pubKey and deposit 1 ether for different withdrawal credential, thereby setting withdrawal credit before deposit of 32 ether by contract and thereby when 32 deposit ether are deposited, the withdrawal credential is also what was set before rather than the one being sent in depositEther transaction

Set withdrawal credentials for validator by depositing 1 ether with desired withdrawal credentials, before adding it in Operator Registry

#0 - FortisFortuna

2022-09-25T22:02:36Z

Interesting point, but at the beginning, the only validators we will have will be Frax controlled.

#1 - joestakey

2022-09-26T18:05:45Z

Similar issue to #108

#2 - 0xean

2022-10-12T17:40:26Z

function deposit( bytes calldata pubkey, bytes calldata withdrawal_credentials, bytes calldata signature, bytes32 deposit_data_root ) override external payable { // Extended ABI length checks since dynamic types are used. require(pubkey.length == 48, "DepositContract: invalid pubkey length"); require(withdrawal_credentials.length == 32, "DepositContract: invalid withdrawal_credentials length"); require(signature.length == 96, "DepositContract: invalid signature length"); // Check deposit amount require(msg.value >= 1 ether, "DepositContract: deposit value too low"); require(msg.value % 1 gwei == 0, "DepositContract: deposit value not multiple of gwei"); uint deposit_amount = msg.value / 1 gwei; require(deposit_amount <= type(uint64).max, "DepositContract: deposit value too high"); // Emit `DepositEvent` log bytes memory amount = to_little_endian_64(uint64(deposit_amount)); emit DepositEvent( pubkey, withdrawal_credentials, amount, signature, to_little_endian_64(uint64(deposit_count)) ); // Compute deposit data root (`DepositData` hash tree root) bytes32 pubkey_root = sha256(abi.encodePacked(pubkey, bytes16(0))); bytes32 signature_root = sha256(abi.encodePacked( sha256(abi.encodePacked(signature[:64])), sha256(abi.encodePacked(signature[64:], bytes32(0))) )); bytes32 node = sha256(abi.encodePacked( sha256(abi.encodePacked(pubkey_root, withdrawal_credentials)), sha256(abi.encodePacked(amount, bytes24(0), signature_root)) )); // Verify computed and expected deposit data roots match require(node == deposit_data_root, "DepositContract: reconstructed DepositData does not match supplied deposit_data_root"); // Avoid overflowing the Merkle tree (and prevent edge case in computing `branch`) require(deposit_count < MAX_DEPOSIT_COUNT, "DepositContract: merkle tree full"); // Add deposit data root to Merkle tree (update a single `branch` node) deposit_count += 1; uint size = deposit_count; for (uint height = 0; height < DEPOSIT_CONTRACT_TREE_DEPTH; height++) { if ((size & 1) == 1) { branch[height] = node; return; } node = sha256(abi.encodePacked(branch[height], node)); size /= 2; } // As the loop should always end prematurely with the `return` statement, // this code should be unreachable. We assert `false` just to be safe. assert(false); }

#3 - 0xean

2022-10-12T17:46:36Z

It is unclear both in the code above for the deposit contract as well as the documentation on keys

https://kb.beaconcha.in/ethereum-2.0-depositing https://kb.beaconcha.in/ethereum-2-keys

How exactly multiple deposits two the same validator using different withdrawal keys would work. While it would make sense that they would allow a one to many mapping, I am unable to confirm or deny this and therefore will leave the risk currently as H on the side of caution.

#4 - trust1995

2022-10-15T22:50:51Z

Strong find. Indeed in ETH specs we can see that in process_deposit(), if the pubkey is already registered, we just increase its balance, not touching the withdrawal_credentials. However the recommended mitigation does not really address the issue IMO, and the detail is quite lacking, so wouldn't call this a superstar submission.

#5 - FortisFortuna

2022-10-16T16:35:48Z

I think it is technically a non-issue because we will be controlling the addition/removal of validators. Should that eventually become open, we will have to look at the entire code from a different perspective to close security holes.

#6 - trust1995

2022-10-16T16:44:34Z

I think it is relevant, because the idea is to make the protocol controlled validators work for the attacker, because they inserted their own withdrawal credentials directly on the deposit contract.

#7 - FortisFortuna

2022-10-16T16:45:53Z

Ohh I see it now. Good point

#9 - FortisFortuna

2022-10-16T21:38:57Z

Since all of the validators are ours and we have the mnemonic, would it still be an issue though? Lido's setup is different https://medium.com/immunefi/rocketpool-lido-frontrunning-bug-fix-postmortem-e701f26d7971

#10 - FortisFortuna

2022-10-16T21:41:51Z

https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#deposits From @0xJM In the scenario that someone frontruns us with a 1 ETH deposit at the same time we do a 32 ETH deposit, their 1 ETH deposit would fail on beaconchain because it would fail bls.Verify. The result would be them losing their 1 ETH.

Our 32 ETH would go through normally and the validator would activate

#11 - 0xean

2022-10-17T12:21:22Z

@FortisFortuna - can you elaborate on why you believe that bls.Verify would fail?

if not bls.Verify(pubkey, signing_root, deposit.data.signature):

#12 - FortisFortuna

2022-10-17T13:44:39Z

From @0xJM

https://github.com/ethereum/staking-deposit-cli/blob/e2a7c942408f7fc446b889097f176238e4a10a76/staking_deposit/credentials.py#L127

the signing root includes the deposit message which has the withdrawal credentials

https://github.com/ethereum/staking-deposit-cli/blob/e2a7c942408f7fc446b889097f176238e4a10a76/staking_deposit/credentials.py#L112

hence bls.Verify would fail on Beaconchain as I mentioned

he consensus spec has that signing_root = compute_signing_root(deposit_message, domain) which is verified against the signature

#13 - 0xean

2022-10-17T13:54:32Z

The signature would be valid. The validator would still sign the message containing the credentials that they are front running with.

#14 - FortisFortuna

2022-10-17T13:59:44Z

From @denett "The signature would be valid. The validator would still sign the message containing the credentials that they are front running with." Only the validator can create a valid signature and we own the key to the validator.

#15 - 0xean

2022-10-17T14:03:59Z

Yea, so this is the root of it, the contest does not specify that Frax is the owner of all validators that are meant to be used with this protocol. Without stating that ahead of time for the Wardens to understand, I believe this to be a valid finding and the warden should be awarded.

#16 - FortisFortuna

2022-10-17T14:16:02Z

Ok. So in our current setup, assuming Frax owns all validators, we are safe?

#17 - 0xean

2022-10-17T14:25:51Z

:) I cannot guarantee anything in DeFi is safe. My understanding of this particular vulnerability is that it would require a validator to act maliciously by using a smaller than 32 ETH deposit to front run your deposit and enable them to control the withdrawal in the future. If the validator is owned by your team and the keys are never exploited, then I don't see how the front ran signature could be generated.

#18 - FortisFortuna

2022-10-17T14:27:08Z

Ya, I hear you lol. At least for this particular scenario we are ok then, according to the known bug. We can pay out for the bug because none of our team were aware of it and it is good to know for the future.

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax © 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter