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: 12/72
Findings: 2
Award: $498.91
🌟 Selected for report: 0
🚀 Solo Findings: 0
490.6256 USDC - $490.63
The issue presents a significant challenge in the redemption mechanism, particularly when an investor's remaining USDC redemption limit is inadvertently less than the minimum redemption threshold. This scenario can lead to a situation where an investor's funds (tracked by OUSG/rOUSG) become effectively "stuck" until the value of the underlying share assets increases sufficiently to meet the minimum redemption amount. The impact of this issue can vary depending on the frequency of such occurrences and the volume of affected transactions. It can lead to liquidity constraints for investors, diminish investor confidence due to perceived inflexibility and lack of liquidity, and introduce operational complexities for both investors and system administrators.
Here's a typical scenario based on the default settings of the contract,
minimumRedemptionAmount = 50_000e6
and, a hypothetical rate limit:
instantRedemptionLimit = 1_000_000e6 currentInstantRedemptionAmount = 935_000e6
getOUSGPrice() == 110e18
.Manual
Consider allowing redemption if the investor's usdcAmountToRedeem
being less than minimumRedemptionAmount
is all the investor has after being converted from the OUSG shares.
Context
#0 - 0xRobocop
2024-04-04T01:27:57Z
Consider QA.
This is more UX rather than a security concern. User could wait until currentInstantRedemptionAmount
resets. Similar root cause to #142
#1 - c4-pre-sort
2024-04-04T01:27:58Z
0xRobocop marked the issue as primary issue
#2 - c4-pre-sort
2024-04-04T01:28:04Z
0xRobocop marked the issue as insufficient quality report
#3 - c4-judge
2024-04-09T12:37:24Z
3docSec marked the issue as duplicate of #142
#4 - c4-judge
2024-04-09T12:37:28Z
3docSec marked the issue as satisfactory
🌟 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
Decreasing an ERC20 token allowance via the approve
function can unwittingly expose transactions to front-running attacks, where malicious actors exploit the delay between transaction submission and confirmation to their advantage. In such scenarios, a spender could detect a pending decrease in their allowance and quickly utilize the existing higher allowance before the approve transaction is confirmed, often by submitting their transaction with a higher gas fee for priority processing.
https://github.com/code-423n4/2024-03-ondo-finance/blob/main/contracts/ousg/rOUSG.sol#L251-L254
function approve(address _spender, uint256 _amount) public returns (bool) { _approve(msg.sender, _spender, _amount); return true; }
https://github.com/code-423n4/2024-03-ondo-finance/blob/main/contracts/ousg/rOUSG.sol#L472-L482
function _approve( address _owner, address _spender, uint256 _amount ) internal whenNotPaused { require(_owner != address(0), "APPROVE_FROM_ZERO_ADDRESS"); require(_spender != address(0), "APPROVE_TO_ZERO_ADDRESS"); allowances[_owner][_spender] = _amount; emit Approval(_owner, _spender, _amount); }
This vulnerability underscores the importance of using the increaseAllowance
and decreaseAllowance
functions, as recommended by the ERC20 standard and secure implementations like OpenZeppelin. These methods offer a more secure approach to adjusting allowances by basing changes on the current allowance level, thereby reducing the susceptibility to front-running and enhancing the overall security of token transactions.
As such, users should be reminded to proceed with cautions when handling spenders' allowances.
The OUSGInstantManager contract employs a simple yet effective rate limiting mechanism through its _checkAndUpdateInstantMintLimit
function, setting a cap on the total amount mintable within defined durations to manage liquidity and mitigate potential abuses.
Here's a hypothetical situation:
minimumDepositAmount
has been assigned 100_000e6, i.e. 100_000 USDC.instantMintLimit
is set to 1 million USDC and resetInstantMintDuration
is set to 1 day.block.timestamp >= lastResetInstantRedemptionTime + resetInstantRedemptionDuration
.Consider introducing an appropriate CUSHION
e.g. 100 USDC or a tiny fraction of instantRedemptionLimit
or minimumDepositAmount
to circumvent the aforementioned exploit:
require( - amount <= instantRedemptionLimit - currentInstantRedemptionAmount, + amount <= instantRedemptionLimit - currentInstantRedemptionAmount + CUSHION, "RateLimit: Redemption exceeds rate limit" );
Note: The same shall also apply to redeem()
that's governed by _checkAndUpdateInstantRedemptionLimit()
.
getOUSGPrice()
with price
fed by Chainlink is frequently called in ousgInstantManager.sol and rOUSG.sol. The system would be crippled if Chainlink failed to feed correct prices due to uncontrolled situations e.g. staleness, outside min/max answers, new integrations needed to accommodate decimals changes initiated by Chainlink etc.
To mitigate the risks associated with relying on a single oracle, such as Chainlink, for price feeds in smart contracts, several strategies can be employed. These include multi-oracle aggregation, which leverages data from multiple sources to derive a more reliable price point; fallback oracles such as Uniswap TWAP, which serve as backups in case the primary oracle fails; oracle heartbeat checks to ensure regular data updates; circuit breakers to halt operations under abnormal data conditions; governance intervention for manual fixes; decentralized oracle networks for broader data aggregation; and cross-referencing with on-chain data for additional validation. Implementing a combination of these methods can significantly enhance the robustness and security of smart contracts, especially in decentralized finance (DeFi) applications, by reducing the dependency on a single data source and mitigating the risks of data manipulation or failure.
Consider introducing view functions in ousgInstantManager.sol on remaining rate limits where possible so that investors may check whether or not their mints/redeems are going to be permissible.
Here're the suggested functions:
function checkInstantMintLimit(uint256 amount) public view returns (bool permissible) { permissible = (amount <= instantMintLimit - currentInstantMintAmount);
function checkInstantRedemptionLimit(uint256 amount) public view returns (bool permissible) { permissible = (amount <= instantRedemptionLimit - currentInstantRedemptionAmount);
MINIMUM_OUSG_PRICE
The oracle contracts are out of scope, but I am noticing MINIMUM_OUSG_PRICE
might be inflated by two decimals:
uint256 public constant MINIMUM_OUSG_PRICE = 105e18;
The RWADynamicOracle contract's price calculation, as defined by its derivePrice
function, uses a compound interest formula that factors in the elapsed time since a range's start, the daily interest rate, and the previous range's closing price. The function employs fixed-point arithmetic for operations, crucial due to Solidity's lack of native floating-point support. The final price is thus influenced by the set daily interest rate, the duration of the elapsed time, and the starting price point for each range. Given these dynamics, it's possible for the contract to output prices exceeding 1.05e18
, especially with higher interest rates or over longer periods. However, reaching or surpassing 105e18
would likely indicate unusually high rates or extended compounding periods, possibly pointing to misconfiguration or an aggressive financial model. The actual price outcomes will ultimately hinge on the specific interest rates and range durations set within the contract, emphasizing the need for careful review and alignment with the intended economic behaviors and use cases.
The sponsor probably meant 1.05e18. Otherwise, OUSD and the shares associated with rOUSG will all be diminished/shifted by two decimals. Setting the MINIMUM_OUSG_PRICE
to 105e18
instead of the intended 1.05e18
may not pose a direct security vulnerability but can significantly impact the contract's functionality and economics. Such a high threshold could inadvertently block minting and redemption processes such as those integrated via a router, skew the token's economic mechanisms and exchange rates, disrupt interactions with other DeFi contracts, and degrade user experience due to unrealistic operational conditions. Correcting this error post-deployment, especially on immutable platforms like Ethereum, would be challenging and costly, necessitating a potential contract migration and updates across dependent systems. This underscores the importance of rigorous review and testing of smart contract parameters to avoid such impactful errors.
getOUSGPrice()
_mint()
having the following code logic,
// Calculate the mint amount based on mint fees and usdc quantity uint256 ousgPrice = getOUSGPrice();
as well as _redeem()
having similar code logic below,
uint256 ousgPrice = getOUSGPrice();
should be prepended with getOUSGPrice()
as modifier for better visibility, cleaner code logic, and earlier revert when need be.
Consider introducing view functions in ousgInstantManager.sol for previewing conversions between OUSG and USDC where possible so that investors may better manage their mints/redeems with the anticipated ousgAmountOut/usdcAmountOut just as it has been introduced in rOUSG.sol:
https://github.com/code-423n4/2024-03-ondo-finance/blob/main/contracts/ousg/rOUSG.sol#L185-L203
/** * @return the amount of tokens in existence. */ function totalSupply() public view returns (uint256) { return (totalShares * getOUSGPrice()) / (1e18 * OUSG_TO_ROUSG_SHARES_MULTIPLIER); } /** * @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 */ function balanceOf(address _account) public view returns (uint256) { return (_sharesOf(_account) * getOUSGPrice()) / (1e18 * OUSG_TO_ROUSG_SHARES_MULTIPLIER); }
Consider having the following code line refactored to accurately portray the supposed message:
require( IERC20Metadata(_ousg).decimals() == IERC20Metadata(_rousg).decimals(), - "OUSGInstantManager: OUSG decimals must be equal to USDC decimals" + "OUSGInstantManager: OUSG decimals must be equal to rOUSG decimals" );
Consider having the logic of a modifier embedded through a private function to reduce contract size if need be. A private
visibility that is more efficient on function calls than the internal
visibility is adopted because the modifier will only be making this call inside the contract.
For instance, the modifier below may be refactored as follows:
+ function _whenMintNotPaused() private view { + require(!mintPaused, "OUSGInstantManager: Mint paused"); + } modifier whenMintNotPaused() { - require(!mintPaused, "OUSGInstantManager: Mint paused"); + _whenMintNotPaused(); _; }
Before deploying your contract, activate the optimizer when compiling using “solc --optimize --bin sourceFile.sol”. By default, the optimizer will optimize the contract assuming it is called 200 times across its lifetime. If you want the initial contract deployment to be cheaper and the later function executions to be more expensive, set it to “ --optimize-runs=1”. Conversely, if you expect many transactions and do not care for higher deployment cost and output size, set “--optimize-runs” to a high number.
module.exports = { solidity: { version: "0.8.16", settings: { optimizer: { enabled: true, runs: 1000, }, }, }, };
Please visit the following site for further information:
https://docs.soliditylang.org/en/v0.5.4/using-the-compiler.html#using-the-commandline-compiler
Here's one example of instance on opcode comparison that delineates the gas saving mechanism:
for !=0 before optimization PUSH1 0x00 DUP2 EQ ISZERO PUSH1 [cont offset] JUMPI after optimization DUP1 PUSH1 [revert offset] JUMPI
Disclaimer: There have been several bugs with security implications related to optimizations. For this reason, Solidity compiler optimizations are disabled by default, and it is unclear how many contracts in the wild actually use them. Therefore, it is unclear how well they are being tested and exercised. High-severity security issues due to optimization bugs have occurred in the past . A high-severity bug in the emscripten -generated solc-js compiler used by Truffle and Remix persisted until late 2018. The fix for this bug was not reported in the Solidity CHANGELOG. Another high-severity optimization bug resulting in incorrect bit shift results was patched in Solidity 0.5.6. Please measure the gas savings from optimizations, and carefully weigh them against the possibility of an optimization-related bug. Also, monitor the development and adoption of Solidity compiler optimizations to assess their maturity.
#0 - c4-pre-sort
2024-04-05T05:31:48Z
0xRobocop marked the issue as sufficient quality report
#1 - c4-judge
2024-04-11T09:42:52Z
3docSec marked the issue as grade-b