57114 sc low inherited setadmin function allows to bypass two step admin ownership transfer mechanism
Submitted on Oct 23rd 2025 at 15:53:46 UTC by @ByteKnight for Audit Comp | Alchemix V3
Report ID: #57114
Report Type: Smart Contract
Report severity: Low
Target: https://github.com/alchemix-finance/v3-poc/blob/immunefi_audit/src/AlchemistCurator.sol
Impacts:
Contract fails to deliver promised returns, but doesn't lose value
Description
Brief/Intro
The AlchemistCurator contract implements a two-step admin ownership transfer mechanism via the transferAdminOwnerShip() and acceptAdminOwnership() functions. However, since AlchemistCurator inherits from PermissionedProxy, it also inherits the setAdmin() function at PermissionedProxy, which allows the current admin to directly change the admin address in a single transaction, completely bypassing the intended two-step transfer security mechanism.
Vulnerability Details
The AlchemistCurator contract tries to implement a secure two-step ownership transfer mechanism:
function transferAdminOwnerShip(address _newAdmin) external onlyAdmin {
pendingAdmin = _newAdmin;
}
function acceptAdminOwnership() external onlyAdmin {
admin = pendingAdmin;
pendingAdmin = address(0);
emit AdminChanged(admin);
}However, AlchemistCurator inherits from PermissionedProxy, which provides a direct single-step admin change function:
This inheritance creates two parallel admin transfer mechanisms:
Two-step mechanism (desired): transferAdminOwnerShip() -> acceptAdminOwnership()
Single-step bypass (inherited): setAdmin()
The presence of the setAdmin() completely undermines the security benefits of the two-step mechanism, as this function can be used intentionally or accidentally to instantly transfer ownership to any address without requiring confirmation from the new admin.
NOTE: This finding is related to but distinct from the Report ID: 56911, which identifies that the two-step pattern itself is incorrectly implemented (using onlyAdmin instead of checking pendingAdmin). This finding demonstrates that even if that issue were fixed, the inherited setAdmin() function would still allow bypassing the two-step mechanism entirely.
Impact Details
Having both a two-step and single-step mechanism creates confusion about which pattern should be used and can lead to instant transfer of a control to an incorrect address (due to typo, copy-paste error, or sending to a contract that cannot manage the protocol), which could lead to the contract being inaccessible.
References
Two-step transfer mechanism: https://github.com/alchemix-finance/v3-poc/blob/a192ab313c81ba3ab621d9ca1ee000110fbdd1e9/src/AlchemistCurator.sol#L27-L35
Single-step transfer mechanism: https://github.com/alchemix-finance/v3-poc/blob/a192ab313c81ba3ab621d9ca1ee000110fbdd1e9/src/utils/PermissionedProxy.sol#L31-L34
Related finding: Report 56911
Proof of Concept
Proof of Concept
To verify the flaw, do the following steps:
In the PermissionedProxy contract change the visibility of the
adminstate variable to public. We only need this to be able to log admin address in the terminal with console2.log();
Add the following test to existing test suite in the AlchemistCurator.t.sol file:
To run the test, run the following script in terminal and paste your fork url:
Was this helpful?