Single-Step Ownership Transfer Can Irreversibly Misconfigure Admin Control
Description
Both StrataxOracle and Stratax implement transferOwnership as an immediate, one-transaction ownership change.
In StrataxOracle, ownership is reassigned directly:
function transferOwnership(address _newOwner) external onlyOwner {
require(_newOwner != address(0), "Invalid address");
address previousOwner = owner;
@> owner = _newOwner;
emit OwnershipTransferred(previousOwner, _newOwner);
}
In Stratax, the same pattern is used:
function transferOwnership(address _newOwner) external onlyOwner {
require(_newOwner != address(0), "Invalid address");
@> owner = _newOwner;
}
Because ownership transfer does not require acceptance by the new owner, any mistake in _newOwner (wrong address, typo, stale deployment address, non-controlled contract, compromised routing flow) becomes final as soon as the transaction is mined.
If ownership is accidentally transferred to an address that cannot or will not operate admin functions, privileged operations become permanently unavailable to the intended operator. This creates governance and operational fragility for critical protocol controls.
In this protocol, owner-only functionality includes:
-
StrataxOracle: setPriceFeed, setPriceFeeds, transferOwnership
-
Stratax: setStrataxOracle, setFlashLoanFee, recoverTokens, unwindPosition, transferOwnership
As a result, a single operational mistake during ownership rotation can block oracle maintenance, fee configuration, token recovery, and position management.
Risk
Likelihood: Medium
This issue is typically triggered by operational error rather than direct external exploitation. However, ownership transfers are high-impact administrative actions, and address-entry mistakes or process misconfigurations are realistic in production operations.
Impact: Medium
If ownership is transferred to an incorrect or unusable address, the protocol can lose effective administrative control over critical parameters and recovery flows. This can degrade incident response and protocol maintenance, and in some scenarios lead to prolonged downtime or locked funds requiring migration.
Proof of Concept
Below is the full PoC code with two tests (for Stratax and StrataxOracle):
contract OwnershipTransferRiskTest is Test {
Stratax internal stratax;
StrataxOracle internal oracle;
address internal realAdmin;
address internal wrongAddress;
function setUp() public {
realAdmin = makeAddr("realAdmin");
wrongAddress = makeAddr("wrongAddress");
oracle = new StrataxOracle();
stratax = new Stratax();
stratax.initialize(
address(0x1001),
address(0x1002),
address(0x1003),
address(0x1004),
address(oracle)
);
}
function test_Stratax_OneStepOwnershipTransferMisconfiguration() public {
stratax.transferOwnership(realAdmin);
vm.prank(realAdmin);
stratax.transferOwnership(wrongAddress);
vm.prank(realAdmin);
vm.expectRevert("Not owner");
stratax.setFlashLoanFee(25);
vm.prank(wrongAddress);
stratax.setFlashLoanFee(25);
assertEq(stratax.flashLoanFeeBps(), 25);
}
function test_StrataxOracle_OneStepOwnershipTransferMisconfiguration() public {
address token = address(0x2001);
address feed = address(0x2002);
vm.mockCall(feed, abi.encodeWithSignature("decimals()"), abi.encode(uint8(8)));
oracle.transferOwnership(realAdmin);
vm.prank(realAdmin);
oracle.transferOwnership(wrongAddress);
vm.prank(realAdmin);
vm.expectRevert("Not owner");
oracle.setPriceFeed(token, feed);
vm.prank(wrongAddress);
oracle.setPriceFeed(token, feed);
assertEq(oracle.priceFeeds(token), feed);
}
}
Run the test with:
forge test --match-path test/unit/OwnershipTransfer.t.sol
Output:
Ran 2 tests for test/unit/OwnershipTransfer.t.sol:OwnershipTransferRiskTest
[PASS] test_StrataxOracle_OneStepOwnershipTransferMisconfiguration() (gas: 57668)
[PASS] test_Stratax_OneStepOwnershipTransferMisconfiguration() (gas: 32336)
Suite result: ok. 2 passed; 0 failed; 0 skipped; finished in 10.06ms (1.42ms CPU time)
Ran 1 test suite in 137.59ms (10.06ms CPU time): 2 tests passed, 0 failed, 0 skipped (2 total tests)
As shown by the output, a single mistaken transferOwnership call immediately removes the intended admin's permissions and transfers control to the mistakenly provided address.
Recommended Mitigation
Use a two-step ownership transfer flow (pendingOwner + acceptOwnership) in both contracts.
For StrataxOracle, for example:
address public owner;
+ address public pendingOwner;
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
+ event OwnershipTransferStarted(address indexed previousOwner, address indexed pendingOwner);
function transferOwnership(address _newOwner) external onlyOwner {
require(_newOwner != address(0), "Invalid address");
- address previousOwner = owner;
- owner = _newOwner;
- emit OwnershipTransferred(previousOwner, _newOwner);
+ pendingOwner = _newOwner;
+ emit OwnershipTransferStarted(owner, _newOwner);
+ }
+
+ function acceptOwnership() external {
+ require(msg.sender == pendingOwner, "Not pending owner");
+ address previousOwner = owner;
+ owner = pendingOwner;
+ pendingOwner = address(0);
+ emit OwnershipTransferred(previousOwner, owner);
}
Apply the same pattern to Stratax and its interface so ownership changes are completed only after explicit acceptance by the recipient.
If desired, this can be replaced with OpenZeppelin Ownable2StepUpgradeable semantics for standardized behavior and reduced custom access-control risk.