Summary
The transferAdmin
function in the Adminable
contract lacks validation against zero address transfers. This oversight allows the admin role to be permanently lost by transferring it to address(0)
, effectively bricking all admin-protected functionality in the contract.
Check code snippet
https://github.com/Cyfrin/2024-10-sablier/blob/main/src/abstracts/Adminable.sol#L33-L41
Vulnerability Details
The vulnerability exists in the Adminable
contract's transferAdmin
function:
function transferAdmin(address newAdmin) public virtual override onlyAdmin {
admin = newAdmin;
emit IAdminable.TransferAdmin({ oldAdmin: msg.sender, newAdmin: newAdmin });
}
The function allows setting admin to any address, including address(0), without validation. Once set to the zero address:
No address can satisfy the onlyAdmin modifier check
The admin role becomes permanently inaccessible
Impact
Permanent Loss of Admin Control - Admin functions become permanently inaccessible, No way to transfer admin role to a new address
Impact
The following Forge test demonstrates the vulnerability:
pragma solidity >=0.8.22;
import { Test } from "forge-std/src/Test.sol";
import "../../src/abstracts/Adminable.sol";
import "../../src/interfaces/IAdminable.sol";
import "../../src/libraries/Errors.sol";
contract MockProtocol is Adminable {
uint256 public value;
bool public isPaused;
constructor() {
admin = msg.sender;
}
function updateValue(uint256 newValue) external onlyAdmin {
value = newValue;
}
function pause() external onlyAdmin {
isPaused = true;
}
}
contract AdminCompromiseTest is Test {
MockProtocol public protocol;
address public initialAdmin;
function setUp() public {
initialAdmin = address(this);
protocol = new MockProtocol();
}
function test_AdminCompromiseByZeroAddress() public {
protocol.transferAdmin(address(0));
assertEq(protocol.admin(), address(0));
vm.expectRevert(abi.encodeWithSelector(
Errors.CallerNotAdmin.selector,
address(0),
initialAdmin
));
protocol.updateValue(100);
vm.startPrank(initialAdmin);
vm.expectRevert(abi.encodeWithSelector(
Errors.CallerNotAdmin.selector,
address(0),
initialAdmin
));
protocol.pause();
vm.stopPrank();
}
}
forge test --match-contract AdminCompromiseTest -vv
Compiler run successful!
Ran 1 test for tests/audit/AdminCompromiseTest.t.sol:AdminCompromiseTest
[PASS] test_AdminCompromiseByZeroAddress() (gas: 21732)
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 695.80µs (205.70µs CPU time)
Ran 1 test suite in 6.09ms (695.80µs CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)
Tools Used
Manual code review, VS Code
Recommendations
Add Zero Address Validation
Implement Two-Step Transfer Process ( if possible )