In the cash part of the code, the contract TokenProxy in Proxy.sol serves as a generic proxy for all tokens deployed using factory contracts.
The factory contracts deploy a ProxyAdmin contract and a TokenProxy, alongside the respective token implementation.
Both changeAdmin(), changeProxyAdmin() or transferOwnership() perform this ownership transition in one single step. Also the renounceOwnership() action can change the ownership of the AdminProxy contract to address(0) without any option of revert.
A malicious admin or an error in the address when calling any of these functions can prevent admin activities on the token and admin contracts forever.
Proof of Concept
Ownable transfer of ownership:
/**
* @dev Leaves the contract without owner. It will not be possible to call
* `onlyOwner` functions anymore. Can only be called by the current owner.
*
* NOTE: Renouncing ownership will leave the contract without an owner,
* thereby removing any functionality that is only available to the owner.
*/
function renounceOwnership() public virtual onlyOwner {
_transferOwnership(address(0));
}
/**
* @dev Transfers ownership of the contract to a new account (`newOwner`).
* Can only be called by the current owner.
*/
function transferOwnership(address newOwner) public virtual onlyOwner {
require(newOwner != address(0), "Ownable: new owner is the zero address");
_transferOwnership(newOwner);
}
/**
* @dev Transfers ownership of the contract to a new account (`newOwner`).
* Internal function without access restriction.
*/
function _transferOwnership(address newOwner) internal virtual {
address oldOwner = _owner;
_owner = newOwner;
emit OwnershipTransferred(oldOwner, newOwner);
}
Or, TransparentUpgradeableProxy transfer of admin role:
/**
* @dev Changes the admin of the proxy.
*
* Emits an {AdminChanged} event.
*
* NOTE: Only the admin can call this function. See {ProxyAdmin-changeProxyAdmin}.
*/
function changeAdmin(address newAdmin) external virtual ifAdmin {
_changeAdmin(newAdmin);
}
/**
* @dev Stores a new address in the EIP1967 admin slot.
*/
function _setAdmin(address newAdmin) private {
require(newAdmin != address(0), "ERC1967: new admin is the zero address");
StorageSlot.getAddressSlot(_ADMIN_SLOT).value = newAdmin;
}
/**
* @dev Changes the admin of the proxy.
*
* Emits an {AdminChanged} event.
*/
function _changeAdmin(address newAdmin) internal {
emit AdminChanged(_getAdmin(), newAdmin);
_setAdmin(newAdmin);
}
Tools Used
Manual analysis.
Recommended Mitigation Steps
Recommend not using the standard Ownable or TransparentUpgradeableProxy contracts directly as they are, and change the transfer of ownership/admin privileges to be a two step process, like it is implemented in other contracts, like contracts/lending/tokens/cCash/CTokenCash.sol, where you have a _setPendingAdmin() and a _acceptAdmin() calls to start and finalize the process.
Lines of code
https://github.com/code-423n4/2023-01-ondo/blob/f3426e5b6b4561e09460b2e6471eb694efdd6c70/contracts/cash/external/openzeppelin/contracts/proxy/ProxyAdmin.sol#L63 https://github.com/code-423n4/2023-01-ondo/blob/f3426e5b6b4561e09460b2e6471eb694efdd6c70/contracts/cash/external/openzeppelin/contracts/proxy/TransparentUpgradeableProxy.sol#L89 https://github.com/code-423n4/2023-01-ondo/blob/f3426e5b6b4561e09460b2e6471eb694efdd6c70/contracts/cash/external/openzeppelin/contracts/access/Ownable.sol#L65 https://github.com/code-423n4/2023-01-ondo/blob/f3426e5b6b4561e09460b2e6471eb694efdd6c70/contracts/cash/external/openzeppelin/contracts/access/Ownable.sol#L57
Vulnerability details
Impact
In the
cash
part of the code, the contractTokenProxy
inProxy.sol
serves as a generic proxy for all tokens deployed usingfactory
contracts.The
factory
contracts deploy aProxyAdmin
contract and aTokenProxy
, alongside the respective token implementation.Both
changeAdmin()
,changeProxyAdmin()
ortransferOwnership()
perform this ownership transition in one single step. Also therenounceOwnership()
action can change the ownership of theAdminProxy
contract toaddress(0)
without any option of revert.A malicious admin or an error in the address when calling any of these functions can prevent admin activities on the token and admin contracts forever.
Proof of Concept
Ownable transfer of ownership:
Or, TransparentUpgradeableProxy transfer of admin role:
Tools Used
Manual analysis.
Recommended Mitigation Steps
Recommend not using the standard
Ownable
orTransparentUpgradeableProxy
contracts directly as they are, and change the transfer of ownership/admin privileges to be a two step process, like it is implemented in other contracts, likecontracts/lending/tokens/cCash/CTokenCash.sol
, where you have a_setPendingAdmin()
and a_acceptAdmin()
calls to start and finalize the process.