Contract fails to deliver promised returns, but doesn't lose value
Description
Brief/Intro
The AlchemistCurator::acceptAdminOwnership() function uses the onlyAdmin modifier, which restricts access to the current admin instead of the pending admin. This breaks the two-step admin transfer pattern and makes it impossible for a pending admin to accept ownership.
Vulnerability Details
The AlchemistCurator contract implements a two-step admin ownership transfer mechanism where:
The current admin calls transferAdminOwnerShip() to set a pending admin
The pending admin should call acceptAdminOwnership() to finalize the transfer
However, the implementation is flawed:
The acceptAdminOwnership() function uses the onlyAdmin modifier, which requires msg.sender == admin (from PermissionedProxy). This means only the current admin can call this function, not the pending admin.
The correct implementation can be seen in other contracts like Transmuter and AlchemistV3:
This vulnerability breaks the guarantee of two-step admin transfers, which is designed to prevent accidental admin changes by requiring explicit acceptance from the new admin.
Impact Details
No pending admin can ever accept ownership since they lack the current admin privileges required by the onlyAdmin modifier.
// SPDX-License-Identifier: MIT
pragma solidity 0.8.28;
import {Test, console} from "forge-std/Test.sol";
import {AlchemistCurator} from "../AlchemistCurator.sol";
contract PoC_AdminOwnershipVulnerability is Test {
AlchemistCurator public curator;
address public currentAdmin = makeAddr("currentAdmin");
address public newAdmin = makeAddr("newAdmin");
address public operator = makeAddr("operator");
function setUp() public {
// Deploy AlchemistCurator with current admin
vm.prank(currentAdmin);
curator = new AlchemistCurator(currentAdmin, operator);
}
function test_AlchemistCurator_PendingAdminCannotAccept() public {
// Current admin initiates transfer to new admin
vm.prank(currentAdmin);
curator.transferAdminOwnerShip(newAdmin);
// The pending admin should be able to accept, but can't due to onlyAdmin modifier
vm.startPrank(newAdmin);
vm.expectRevert(bytes("PD")); // PermissionedProxy "Permission Denied"
curator.acceptAdminOwnership();
vm.stopPrank();
}
}