Platform: Code4rena
Start Date: 29/03/2024
Pot Size: $36,500 USDC
Total HM: 5
Participants: 72
Period: 5 days
Judge: 3docSec
Total Solo HM: 1
Id: 357
League: ETH
Rank: 68/72
Findings: 1
Award: $8.28
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: immeas
Also found by: 0xAkira, 0xCiphky, 0xGreyWolf, 0xJaeger, 0xMosh, 0xabhay, 0xlemon, 0xmystery, 0xweb3boy, Aamir, Abdessamed, Aymen0909, Breeje, DanielArmstrong, DarkTower, Dots, EaglesSecurity, FastChecker, HChang26, Honour, IceBear, JC, K42, Krace, MaslarovK, Omik, OxTenma, SAQ, Shubham, Stormreckson, Tigerfrake, Tychai0s, VAD37, ZanyBonzy, albahaca, arnie, ast3ros, asui, b0g0, bareli, baz1ka, btk, caglankaan, carrotsmuggler, cheatc0d3, dd0x7e8, grearlake, igbinosuneric, jaydhales, kaden, kartik_giri_47538, m4ttm, ni8mare, niser93, nonn_ac, oualidpro, pfapostol, pkqs90, popeye, radev_sw, samuraii77, slvDev, zabihullahazadzoi
8.2807 USDC - $8.28
rOUSG
contract isn't sanitized with MINIMUM_OUSG_PRICE
as in OUSGInstantManager
contract.The oracle returned by the oracle in OUSGInstantManager
contract is correctly sanitized with MINIMUM_OUSG_PRICE
but it is left unsanitized in the rOUSG
contract, which may cause protocol to loss OUSG when the price goes below the minimum price OUSG than protocol decided price by MINIMUM_OUSG_PRICE
.
https://github.com/code-423n4/2024-03-ondo-finance/blob/main/contracts/ousg/rOUSG.sol#L378
function getOUSGPrice() public view returns (uint256 price) { (price, ) = oracle.getPriceData(); }
Add this require statement:
function getOUSGPrice() public view returns (uint256 price) { (price, ) = oracle.getPriceData(); @> require( price > MINIMUM_OUSG_PRICE, "OUSGInstantManager::getOUSGPrice: Price unexpectedly low" ); }
While getOUSDPrice
functions for Oracle price perfectly account for lower limit of oracle price but upper limit wasn't checked properly which may lead protocol facing unwanted risk when the oracle price goes too above than a valid range.
function getOUSGPrice() public view returns (uint256 price) { (price, ) = oracle.getPriceData(); require( price > MINIMUM_OUSG_PRICE, "OUSGInstantManager::getOUSGPrice: Price unexpectedly low" ); // @audit no upper limit }
Add a valid upper limit similar to minimum price.
setMinimumBUIDLRedemptionAmount
doesn't check if _minimumRedemptionAmount
is lower than FEE_GRANULARITY
The function setMinimumBUIDLRedemptionAmount
should if the new buidl redemption amount is greater than FEE_GRANULARITY
because FEE_GRANULARITY
is divided each time fee is calculated, if not checked it will lead to 0 fee.
function setMinimumBUIDLRedemptionAmount( uint256 _minimumBUIDLRedemptionAmount ) external override onlyRole(DEFAULT_ADMIN_ROLE) { emit MinimumBUIDLRedemptionAmountSet( minBUIDLRedeemAmount, _minimumBUIDLRedemptionAmount ); minBUIDLRedeemAmount = _minimumBUIDLRedemptionAmount; }
The function setMinimumRedemptionAmount
in the contract perfectly check this statement. To fix it similarly, add this line:
require( _minimumRedemptionAmount >= FEE_GRANULARITY, "setMinimumRedemptionAmount: Amount too small" );
If Ondo team changes the KYC status of user to false then she/he can frontrun the transaction and perform redeeming/minting/burning/transferring shares functions before his/her KYC de-allocation.
_beforeTokenTransfer
is called before mint
, burn
, transferShares
functions in ROUSG
contract.
https://github.com/code-423n4/2024-03-ondo-finance/blob/main/contracts/ousg/rOUSG.sol#L586
function _beforeTokenTransfer( address from, address to, uint256 ) internal view { // Check constraints when `transferFrom` is called to facliitate // a transfer between two parties that are not `from` or `to`. if (from != msg.sender && to != msg.sender) { // can send to msg.sender to msg.sender require(_getKYCStatus(msg.sender), "rOUSG: 'sender' address not KYC'd"); } if (from != address(0)) { // If not minting require(_getKYCStatus(from), "rOUSG: 'from' address not KYC'd"); } if (to != address(0)) { // If not burning require(_getKYCStatus(to), "rOUSG: 'to' address not KYC'd"); } }
Make sure when user perform redeeming/minting/burning/transferring shares, there is a delay before performing the functions again so that Ondo Team can set KYC status of user to false.
retrieveTokens
functionThe function retrieveTokens
can drain the all the tokens to a admin controlled address which is a definite centralization risk for its user.
function retrieveTokens( address token, address to, uint256 amount ) external onlyRole(DEFAULT_ADMIN_ROLE) { IERC20(token).transfer(to, amount); // @audit Centralization risk [low] }
Remove the function to avoid the risk.
safeMint
instead of mint
_mint()
is discouraged in favor of _safeMint() which ensures that the recipient is either an EOA or implements IERC721Receiver. Both OpenZeppelin and solmate have versions of this function so that NFTs aren't lost if they're minted to contracts that cannot transfer them back out.
function _mint( uint256 usdcAmountIn, address to ) internal returns (uint256 ousgAmountOut) { require( IERC20Metadata(address(usdc)).decimals() == 6, "OUSGInstantManager::_mint: USDC decimals must be 6" ); ... ... ousg.mint(to, ousgAmountOut); // use safemint instead of mint. @audit }
Use OpenZeppelin's _safeMint
Before calling a low level call to a contract it should check the if contract code exists or not.
function multiexcall( ExCallData[] calldata exCallData ) external payable override onlyRole(DEFAULT_ADMIN_ROLE) returns (bytes[] memory results) { results = new bytes[](exCallData.length); for (uint256 i = 0; i < exCallData.length; ++i) { @> (bool success, bytes memory ret) = address(exCallData[i].target).call{ value: exCallData[i].value }(exCallData[i].data); require(success, "Call Failed"); results[i] = ret; } }
https://github.com/code-423n4/2024-03-ondo-finance/blob/main/contracts/ousg/rOUSGFactory.sol#L121
function multiexcall( ExCallData[] calldata exCallData ) external payable override onlyGuardian returns (bytes[] memory results) { results = new bytes[](exCallData.length); for (uint256 i = 0; i < exCallData.length; ++i) { (bool success, bytes memory ret) = address(exCallData[i].target).call{ value: exCallData[i].value }(exCallData[i].data); require(success, "Call Failed"); results[i] = ret; } }
Upgrading to a newer Solidity release can optimize gas usage, take advantage of new features and improve overall contract efficiency. Where possible, based on compatibility requirements, it is recommended to use newer/latest solidity version to take advantage of the latest optimizations and features.
pragma solidity 0.8.16;
https://github.com/code-423n4/2024-03-ondo-finance/blob/main/contracts/ousg/rOUSG.sol#L16
pragma solidity 0.8.16;
Use latest version of solidity which is above 0.8.20
According to Solidity doc, it is not recommended to use assert()
Assert should only be used to test for internal errors, and to check invariants. Properly functioning code should never create a Panic, not even on invalid external input. If this happens, then there is a bug in your contract which you should fix.
https://github.com/code-423n4/2024-03-ondo-finance/blob/main/contracts/ousg/rOUSGFactory.sol#L94
assert(rOUSGProxyAdmin.owner() == guardian);
Use require/revert
instead of assert
It is recommended to use natspec documentation for function. It is missing in the multiexcall
function of OUSGInstantManager
contract.
/*////////////////////////////////////////////////////////////// Miscellaneous //////////////////////////////////////////////////////////////*/ // @audit missing natspac [nc] function multiexcall( ExCallData[] calldata exCallData ) external payable override onlyRole(DEFAULT_ADMIN_ROLE) returns (bytes[] memory results) { results = new bytes[](exCallData.length); for (uint256 i = 0; i < exCallData.length; ++i) { (bool success, bytes memory ret) = address(exCallData[i].target).call{ value: exCallData[i].value }(exCallData[i].data); require(success, "Call Failed"); results[i] = ret; } }
Add natspec documenation so that developers can understand the function properly.
According to the Solidity Style Guide, contracts and libraries should be named using the CapWords style and match their filenames.
https://github.com/code-423n4/2024-03-ondo-finance/blob/main/contracts/ousg/rOUSG.sol#L55
contract ROUSG is
Should change the contract name to Rousg
instead.
Even if the function follows the best practice of check-effects-interaction, not using a reentrancy guard when there may be transfer hooks opens the users of this protocol up to read-only reentrancy vulnerability with no way to protect them except by block-listing the entire protocol.
https://github.com/code-423n4/2024-03-ondo-finance/blob/main/contracts/ousg/rOUSG.sol#L431
function unwrap(uint256 _rOUSGAmount) external whenNotPaused { ... }
https://github.com/code-423n4/2024-03-ondo-finance/blob/main/contracts/ousg/rOUSG.sol#L415
function wrap(uint256 _OUSGAmount) external whenNotPaused { require(_OUSGAmount > 0, "rOUSG: can't wrap zero OUSG tokens"); ... }
Use nonReentrant
Modifier of Openzeppelin in both of these two function.
Remove unnecessary commented code section should be deleted.
https://github.com/code-423n4/2024-03-ondo-finance/blob/main/contracts/ousg/rOUSG.sol#L608C1-L661C2
/** * @notice Sets the Oracle address * @dev The new oracle must comply with the IRWAOracle interface * @param _oracle Address of the new oracle */ function setOracle(address _oracle) external onlyRole(DEFAULT_ADMIN_ROLE) { emit OracleSet(address(oracle), _oracle); oracle = IRWAOracle(_oracle); } /** * @notice Admin burn function to burn rOUSG tokens from any account * @param _account The account to burn tokens from * @param _amount The amount of rOUSG tokens to burn * @dev Transfers burned shares (OUSG) to `msg.sender` */ function burn( address _account, uint256 _amount ) external onlyRole(BURNER_ROLE) { uint256 ousgSharesAmount = getSharesByROUSG(_amount); if (ousgSharesAmount < OUSG_TO_ROUSG_SHARES_MULTIPLIER) revert UnwrapTooSmall(); _burnShares(_account, ousgSharesAmount); ousg.transfer( msg.sender, ousgSharesAmount / OUSG_TO_ROUSG_SHARES_MULTIPLIER ); emit Transfer(address(0), msg.sender, getROUSGByShares(ousgSharesAmount)); emit TransferShares(_account, address(0), ousgSharesAmount); } function pause() external onlyRole(PAUSER_ROLE) { _pause(); } function unpause() external onlyRole(DEFAULT_ADMIN_ROLE) { _unpause(); } function setKYCRegistry( address registry ) external override onlyRole(CONFIGURER_ROLE) { _setKYCRegistry(registry); } function setKYCRequirementGroup( uint256 group ) external override onlyRole(CONFIGURER_ROLE) { _setKYCRequirementGroup(group); }
The comment in this natspec is incorrect. It should be
- @dev Balances are dynamic and equal to the
_account
's OUSG shares multiplied
https://github.com/code-423n4/2024-03-ondo-finance/blob/main/contracts/ousg/rOUSG.sol#L196
/** * @return the amount of tokens owned by the `_account`. * * @dev Balances are dynamic and equal the `_account`'s OUSG shares multiplied * by the price of OUSG // @audit typo */ function balanceOf(address _account) public view returns (uint256) { return (_sharesOf(_account) * getOUSGPrice()) / (1e18 * OUSG_TO_ROUSG_SHARES_MULTIPLIER); }
_mintShare
is unnecessary as it is there with wrap()
function.As we see these two functions warp
and _mintShares
are enforcing same whenNotPaused
modifier which is unnecessary. We can remove the modifier from the _mintShares
function.
https://github.com/code-423n4/2024-03-ondo-finance/blob/main/contracts/ousg/rOUSG.sol#L415
warp
:
function wrap(uint256 _OUSGAmount) external whenNotPaused { require(_OUSGAmount > 0, "rOUSG: can't wrap zero OUSG tokens"); ... }
https://github.com/code-423n4/2024-03-ondo-finance/blob/main/contracts/ousg/rOUSG.sol#L529
_mintShares
:
function _mintShares( address _recipient, uint256 _sharesAmount @> ) internal whenNotPaused returns (uint256) { require(_recipient != address(0), "MINT_TO_THE_ZERO_ADDRESS"); }
Remove the whenNotPaused
in _mintShares
function.
When importing from other contracts, specific parameters which are used in the contract that imports should be taken from the contract. It is risky to import all the parameters of a contract.
https://github.com/code-423n4/2024-03-ondo-finance/blob/main/contracts/ousg/rOUSG.sol#L18C1-L26C46
import "contracts/external/openzeppelin/contracts/token/IERC20.sol"; import "contracts/external/openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; import "contracts/external/openzeppelin/contracts-upgradeable/token/ERC20/IERC20MetadataUpgradeable.sol"; import "contracts/external/openzeppelin/contracts-upgradeable/proxy/Initializable.sol"; import "contracts/external/openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol"; import "contracts/external/openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol"; import "contracts/external/openzeppelin/contracts-upgradeable/access/AccessControlEnumerableUpgradeable.sol"; import "contracts/kyc/KYCRegistryClientUpgradeable.sol"; import "contracts/rwaOracles/IRWAOracle.sol";
import "contracts/external/openzeppelin/contracts/access/AccessControlEnumerable.sol"; import "contracts/external/openzeppelin/contracts/security/ReentrancyGuard.sol"; import "contracts/external/openzeppelin/contracts/token/IERC20Metadata.sol"; import "contracts/ousg/rOUSG.sol"; import "contracts/interfaces/IRWALike.sol"; import "contracts/interfaces/IBUIDLRedeemer.sol"; import "contracts/InstantMintTimeBasedRateLimiter.sol"; import "contracts/interfaces/IOUSGInstantManager.sol"; import "contracts/interfaces/IMulticall.sol"; import "contracts/interfaces/IInvestorBasedRateLimiter.sol";
#0 - c4-pre-sort
2024-04-05T04:50:13Z
0xRobocop marked the issue as sufficient quality report
#1 - c4-judge
2024-04-10T07:31:08Z
3docSec marked the issue as grade-b