RWA Security Review: Ondo Protocol Audit
Cantina Security Report
Organization
- @Ondofinance
Engagement Type
Spearbit Web3
Period
-
Repositories
N/A
Researchers
Findings
Medium Risk
1 findings
1 fixed
0 acknowledged
Low Risk
6 findings
5 fixed
1 acknowledged
Informational
11 findings
8 fixed
3 acknowledged
Gas Optimizations
7 findings
6 fixed
1 acknowledged
Medium Risk1 finding
Incorrect condition would cause DOS on subscription and redemption
Severity
- Severity: Medium
Submitted by
Anurag Jain
Description
User's
effective USD value
applicable for fees is determined via_getEffectiveUSDValueAndUpdateUserFeeConfig
function. This function will currently fail under certain circumstances forUserFeeMode.NO_FEE_AFTER_LIMIT
mode.Proof of Concept
- Assume that for User
A
, currentuserFeeConfig
is :
limitVolume : 10currentVolume : 4userFeeMode : UserFeeMode.NO_FEE_AFTER_LIMIT-
User
A
now subscribes for same asset with deposit amount7
(say post fees). This will cause net volume to go abovelimitVolume
(4+7>10), so fee should only be taken on remaining limit which is10-4=6
-
effectiveUSDValue
is calculated as4-10
instead of10-4
which causes underflow error
if (userFeeConfig.currentVolume + usdValue > userFeeConfig.limitVolume) {effectiveUSDValue =userFeeConfig.currentVolume -userFeeConfig.limitVolume;}Coded POC
Add this test in
forge-tests\xManager\OndoFees.t.sol
function test_POC() public {(, OndoFees.UserFeeConfig memory userFeeConfig) = ondoFees.getActiveFeeConfig(flatFeeRWAToken, address(USDC), aliceID);uint256 userVolumeWindow = 100;ondoFees.setUserFee(flatFeeRWAToken,aliceID,userFeeConfig.currentVolume,OndoFees.UserFeeConfig(OndoFees.FeeConfig(true, 10e18, 0),OndoFees.UserFeeMode.NO_FEE_AFTER_LIMIT,50e18, // limitVolume0, // currentVolume0, // lastResetuserVolumeWindow // volumeWindow, seconds));vm.warp(userVolumeWindow + 1);ondoFees.getAndUpdateFee(flatFeeRWAToken,address(USDC),aliceID,49e18);vm.expectRevert();ondoFees.getAndUpdateFee(flatFeeRWAToken,address(USDC),aliceID,2e18);}Impact
This causes both subscription and redemption for rwaToken to fail
Recommendation
Change
NO_FEE_AFTER_LIMIT
condition toelse if (userFeeConfig.userFeeMode == UserFeeMode.NO_FEE_AFTER_LIMIT) {...if (userFeeConfig.currentVolume + usdValue > userFeeConfig.limitVolume) {-- effectiveUSDValue = userFeeConfig.currentVolume - userFeeConfig.limitVolume;++ effectiveUSDValue = userFeeConfig.limitVolume - userFeeConfig.currentVolume;} else {...
Low Risk6 findings
Precision loss in rOUSG share calculation allows subscriptions to exceed slippage tolerance
Severity
- Severity: Low
≈
Likelihood: Medium×
Impact: Low Submitted by
HickupHH3
Description
In
subscribeRebasingOUSG()
, the minimum amount check is performed before several decimal conversions and share calculations, which can lead to precision loss. The actual rOUSG amount received may be less than the specifiedminimumRwaReceived
due to rounding ingetSharesByROUSG()
and the OUSG to rOUSG share conversion.Impact
Users may receive less rOUSG than their specified minimum, effectively bypassing their slippage protection.
Proof of Concept
function test_subscribeRebasingOUSGMRealPrices() public {uint256 aliceDepositAmount = 100_000e6;deal(address(USDC), address(alice), aliceDepositAmount);vm.startPrank(OUSG_GUARDIAN);// use live priceousgOracleWrapper.setRwaOracle(0x0502c5ae08E7CD64fe1AEDA7D6e229413eCC6abe);// set ROUSG oracle to live pricerOUSGToken.setOracle(0x0502c5ae08E7CD64fe1AEDA7D6e229413eCC6abe);vm.startPrank(alice);USDC.approve(address(ousgXManager), aliceDepositAmount);uint256 rousgAmountOut = ousgXManager.subscribeRebasingOUSG(address(USDC),aliceDepositAmount,1000e18);// assert minimum amount receivedassertGe(1000e18, rousgAmountOut); // this will revert}The POC reverts with a failing assertion. `[FAIL: assertion failed: 1000000000000000000000 < 99999999999999999999903]`Recommendation
Consider adding a direct slippage check at the end against
minimumRwaReceived
(which should be renamed tominimumROUSGReceived
for clarity).if (rousgAmountOut < minimumRwaReceived) revert RwaReceiveAmountTooSmall();Unconditional rounding up pulls in 1 wei more for divisible USDS amounts
Severity
- Severity: Low
Submitted by
HickupHH3
Description
The conversion from USDS to USDC unconditionally adds 1 after division. This means that even when the USDS amount is perfectly divisible by the conversion rate, the contract will still pull 1 extra wei of USDC, leading to dust amounts being left in the contract.
This is especially important for when
BuidlUSDCSource
is theUSDCSource
. Should there be aBUIDL
redemption required:_redeemBUIDL(requestedWithdrawAmount - usdcBalanceBefore)
, the unconditional round-up will cause the redemption to fail because there isn't sufficientBUIDL
for the additional requested USDC wei.Recommendation
Use a ceiling division formula that only rounds up when necessary.
// ref: https://github.com/balancer/balancer-v2-monorepo/blob/master/pkg/solidity-utils/contracts/math/FixedPoint.sol#L61-L64// divUp(x, y) := (x + y - 1) / yuint256 usdcAmountNeeded = (usdsAmountNeeded +usdcToUsdsDecimalConversionRate - 1) / usdcToUsdsDecimalConversionRate;Setting limits should revive capacityUsed accordingly
State
- Acknowledged
Severity
- Severity: Low
Submitted by
Anurag Jain
Description
Currently, if
GlobalSubscriptionLimit/GlobalRedemptionLimit/UserSubscriptionRateLimit/UserRedemptionRateLimit
is set byCONFIGURER_ROLE
thencapacityUsed
is reset to0
.This can cause issues since User might have already exhausted their limits and this call revive full limit without waiting for
window
periodProof of Concept
- Attacker frontrun
setGlobalSubscriptionLimit
call and subscribe fully to current limit setGlobalSubscriptionLimit
call executes and setsnewLimit
. At same time it also setscapacityUsed
as0
- Attacker can again subscribe with the
newLimit
instantly without need to wait
Recommendation
New
capacityUsed
can be set as perCurrent capacity used percentage
. Example:- If old limit was
100
and50%
capacity was used - Now if limit is changed to
200
then newcapacityUsed
can be set to100 (50%)
Ondo Finance
Acknowledged. If we are ever concerned about the limits being abused during a change, we can pause the manager contracts prior to setting the new limits. Regardless, when setting new rate limits we can always be aware that this may happen and simply be prepared to process more funds as opposed to adding contract complexity.
availableToWithdraw does not check paused state
Severity
- Severity: Low
Submitted by
carrotsmuggler
Description
Due to how the
OndoTokenRouter
contract is set up, it expects the result of theavailableToWithdraw
call to be available for withdrawal from all the token sources. If theavailableToWithdraw
function returns a non-zero value but this amount is not actually obtainable by calling thetokenSource.withdrawToken
function, it can lead to reverts or broken accounting.uint256 amountAvailable = tokenSource.availableToWithdraw(outputToken);uint256 withdrawAmount = amountAvailable > requestedWithdrawAmount? requestedWithdrawAmount: amountAvailable;// INVARIANT - `TokenSource.withdrawToken` will always withdraw the amount requested// or revert.tokenSource.withdrawToken(outputToken, withdrawAmount);Some of the token source contracts are pausable. In case they are paused, the amount reported should be 0, so that the router does not try to extract tokens from it.
The
BuidlUSDCSource
contract reports the available liquidity in the BUIDL settlement contract but doesn't check if the contracts are paused, and thus, if this amount is actually obtainable. If the BUIDL redemption is paused, thewithdrawToken
calls to this token source contract can revert.Similarly, the
PSMSource
contract uses the USDS PSM module which implements atin
parameter which assigns the paused state. This can be verified here at this address: 0xA188EEC8F81263234dA3622A406892F3D630f98c. Calls to this contract can also revert when it is paused.Proof of Concept
Assume the
BuidlUSDCSource
has 100 USDC tokens, 900 BUIDL tokens and the BUIDL settlement contract has enough liquidity but is paused.The
availableToWithdraw
function will report 1000 USDC.However the contract can only dispense up to 100 USDC. Any more and the
redeem
function will be called on the BUIDL redeemer, and the transaction will revert since it is paused.Recommendation
Check the paused state of underlying contracts. If paused, only the current USDC balance in the token source contract is available and any redemptions/swaps are offline.
PSMSource.sol:availableToWithdraw does not check available USDS liquidity
Severity
- Severity: Low
Submitted by
carrotsmuggler
Description
The USDS peg stabilization module can be used to swap USDS for USDC tokens and vice versa at 1:1 ratios. The current USDS PSM actually just uses the older DAI PSM module underneath it.
Thus, during a USDC->USDS swap, the following steps take place:
- USDC is transferred from the user to the USDS PSM contract at 0xA188EEC8F81263234dA3622A406892F3D630f98c.
- The USDS PSM contract uses the USDC to buy DAI from the DAI PSM contract at 0xf6e72Db5454dd049d0788e411b06CfAF16853042.
- The DAI is then migrated to USDS.
Thus, this flow only works as long as there is enough DAI in the DAI PSM module.
The
availableToWithdraw
in this contract however reports the amount of USDS available as the USDC balance of the contract itself. This can be incorrect in case not enough DAI is available in the PSM contracts.The
availableToWithdraw
reports a value higher than is actually available, which can lead to reverts in the Ondo token router contract since it always expects the reported available balance to be withdrawable.Recommendation
When reporting the available amount of USDS tokens, the amount of DAI available in the DAI PSM contract needs to be considered.
BuidlUSDCSource can have fewer BUIDL tokens than the minimum redeem criteria
Severity
- Severity: Low
Submitted by
carrotsmuggler
Description
The
BuidlUSDCSource
contract reports the amount of USDC tokens it can provide through itsavailableToWithdraw
function, and redeems the BUIDL tokens it holds in order to get this USDC amount.Due to how the Ondo token router is set up, the amount reported by
availableToWithdraw
must always be available for withdrawal from the token contract. However this can get violated if the contract is holding less BUIDL tokens than the minimum redeem limit.The
availableToWithdraw
simply reports the current BUIDL balance of the contract, expecting it all to be available for withdrawal.uint256 availableRedeemerLiquidity = ISettlement(buidlRedeemer.settlement()).availableLiquidity();uint256 availableBuidlRedemptionTotal = currentBuidlBalance >availableRedeemerLiquidity? availableRedeemerLiquidity: currentBuidlBalance;return currentUSDCBalance + availableBuidlRedemptionTotal;But if the contract holds less than the minimum BUIDL redemption amount, none of its BUIDL tokens are actually redeemable. In that case, the contract will be over-reporting the amount of USDC liquidity it can provide, and will thus revert if the reported amount is attempted to be withdrawn from it.
Proof of Concept
Assume the contract has 0 USDC and 190k BUIDL tokens. Assume the minimum redemption limit is 100k BUIDL.
When a user calls to withdraw 90k USDC tokens, the router calls this contract to redeem 100k BUIDL tokens for 100k USDC tokens.
After paying out the user the contract is left with 10k USDC and 90k BUIDL tokens.
Now, the
availableToWithdraw
reports that it can provide 10k+90k = 100k of liquidity. But if this 100k liquidity is to be withdrawn, the contract needs to redeem the 90k BUIDL balance, which is impossible since it falls below the minimum redemption limit.Thus the contract can only provide up to 10k USDC.
Recommendation
In the
availableToWithdraw
function, add the following:currentBuidlBalance = currentBuidlBalance >= minBUIDLRedeemAmount ? currentBuidlBalance : 0;This will make sure the BUIDL balance is only considered if it is actually redeemable.
Informational11 findings
Comment and Variable Improvements
Severity
- Severity: Informational
Submitted by
HickupHH3
Description
The referenced lines have typos or incorrect comments or variables that can be modified for better clarity.
Recommendation
- 2) Will transfer ownership of the proxyAdmin to guardian address.+ 2) Will transfer ownership of the proxyAdmin to `admin` address.- demoninated+ denominated- minimumRwaReceived+ minimumRousgReceived- * @param minimumRwaReceived Minimum amount of RWA to receive, in decimals of rOUSG+ * @param minimumRousgReceived Minimum amount of rOUSG to receive- minimumOusgAmonut+ minimumOusgAmount- rwaAmount+ rousgAmount- Constuctor+ Constructor- avaialable+ available- * @param _minimumTokenPrice The minimum price for the token, denoted in USD with 18 decimals+ * @param _minimumTokenPrice The minimum price for the token, denoted in USD with oracle decimalsConsider allowing current used capacity to be carried over
State
- Acknowledged
Severity
- Severity: Informational
Submitted by
HickupHH3
Description
capacityUsed
is always reset wheneversetGlobalRedemptionLimit()
,setUserSubscriptionRateLimit()
orsetUserRedemptionRateLimit()
is called. There could be a use-case for the limit and / or window to be changed whilst leaving the current capacity used intact. As such, it would be great for a boolean flag to be passed to indicate whether or not to reset the current capacity.Recommendation
Here's an example for
setGlobalRedemptionLimit()
, where a booleanrolloverCapacity
is added.uint256 globalCurrentCapacityUsed;if (rolloverCapacity) {RateLimit memory globalRl = globalSubscriptionLimits[rwaToken];(globalCurrentCapacityUsed, ) = _calculateDecay(globalRl.capacityUsed,globalRl.lastUpdated,globalRl.limit,globalRl.window);}globalSubscriptionLimits[rwaToken] = RateLimit({capacityUsed: globalCurrentCapacityUsed;...});Ondo Finance
Acknowledged. Solid suggestion, but think it adds more complexity to the code than it's worth - we'd also have to ensure that the capacity being rolled over is less than the new limit to avoid unexpected behavior. If there's ever a case where operationally we can't handle an effective higher limit due to the capacity being nulled out (highly highly unlikely), we could instead temporarily pause the contracts.
Missing interface inheritance of PauseManager
Severity
- Severity: Informational
Submitted by
HickupHH3
Description
The
PauseManager
contract imports but does not inherit fromIPauseManager
. Additionally, the interface contains outdated function signatures that don't match the actual implementation (e.g.,pauseSpecificContract()
vspauseContract()
).Recommendation
IPauseManager
should be updated, and havePauseManager
inherit it.FEE_PRECISION is of inconsistent precision
Severity
- Severity: Informational
Submitted by
HickupHH3
Description
OndoFees
usesFEE_PRECISION = 1e18
for basis point calculations, while other contracts in the system use the standard1e4
(10_000) for basis points.Recommendation
Align with the standard basis point precision used throughout the system:
- uint256 public constant FEE_PRECISION = 1e18;+ uint256 public constant FEE_PRECISION = 1e4;Enforce both minimumTokenPrice and oracle are set
Severity
- Severity: Informational
Submitted by
Anurag Jain
Description
If a
minimumTokenPrice[token]
is set, then it must be always enforced. Although it was observed that ifaddress(tokenToAggregatorV3Oracle[token].oracle
is unset thenminimumTokenPrice
is not respected and operation continues without minimum priceProof of Concept
- Observe the
_assertTokenMinimumPrice
function
function _assertTokenMinimumPrice(address token) internal view {// If the minimum price and/or oracle is not set, then we don't need to check the minimum price.if (minimumTokenPrice[token] == 0) return;if (address(tokenToAggregatorV3Oracle[token].oracle) == address(0)) return;...}- Let's say
minimumTokenPrice[token]
was set asX
butaddress(tokenToAggregatorV3Oracle[token].oracle) = address(0)
- In this case,
_assertTokenMinimumPrice
will return without throwing any error which means minimum price check was not performed
Recommendation
- If
minimumTokenPrice[token]
is set then revert ifaddress(tokenToAggregatorV3Oracle[token].oracle
is unset. - Otherwise ensure that both
minimumTokenPrice[token]
andaddress(tokenToAggregatorV3Oracle[token].oracle
are set together
Max cap can be set on feeConfig.bpsFee
Severity
- Severity: Informational
Submitted by
Anurag Jain
Description
There is currently on max cap set on
feeConfig.bpsFee
for all fee modes. If set to a a very high value then User could lose most of deposit funds towards feesRecommendation
Apply a max fee cap on
feeConfig.bpsFee
for all fees modeORACLE_SETTER_ROLE can change prices even on Freeze situation
State
- Acknowledged
Severity
- Severity: Informational
Submitted by
Anurag Jain
Impacted Code
Description
If due to some situation,
freezePrice
is called thencurrentYearlyRate
is set to0
function freezePrice() external onlyRole(PRICE_FREEZER_ROLE) {_syncRateAndPriceStoredToNow();currentYearlyRate = 0;...}This does 2 things:
- Prevents setter from changing rates using
setRateNow
orsetNextRate
since below check always fails
if (rateChange > (MAX_RATE_CHANGE_BPS * currentYearlyRate) / BPS_PRECISION) revert RateChangeTooLarge();// Always fail since currentYearlyRate=0-
But it seems that
ORACLE_SETTER_ROLE
can still change current price by usingsetPriceNow
max uptoMAX_PRICE_CHANGE_BPS
which is currently set to2%
-
Ideally only Admin should be able to change rate or prices in case
freezePrice
has been called. In current code, changing rate can only be done byAdmin
but prices can changed byORACLE_SETTER_ROLE
as well
Recommendation
Do not allow
ORACLE_SETTER_ROLE
to change prices if freezing period is ongoing (i.e. currentYearlyRate=0)Ondo Finance
Acknowledged. Freeze price is mainly made for the case where the rate is set far too high and we need to stop accrual. Given this, adjusting the price within the set limits is fine after a freeze.
defaultUserSubscriptionLimitConfigs is only applicable for new Users
State
- Acknowledged
Severity
- Severity: Informational
Submitted by
Anurag Jain
Description
Changes to
defaultUserSubscriptionLimitConfigs
will only be applicable to new users and old users limit wont be changed.Proof of Concept
- Default user limit for
defaultUserSubscriptionLimitConfigs
is currently set to10M
- User
A
subscribes and since he is a new User, he gets limit of10M
RateLimit storage userRl = transactionType == TransactionType.SUBSCRIPTION? userSubscriptionLimits[rwaToken][userID]: userRedemptionLimits[rwaToken][userID];// If the user rate limit has not been set, instantiate it with the default configurationif (userRl.lastUpdated == 0)_instantiateUserRateLimits(transactionType, rwaToken, userID);- Limit for
defaultUserSubscriptionLimitConfigs
is changed to5M
function setDefaultUserSubscriptionLimitConfig(address rwaToken,uint256 limit,uint48 window) external onlyRole(CONFIGURER_ROLE) {if (rwaToken == address(0)) revert RWAAddressCantBeZero();defaultUserSubscriptionLimitConfigs[rwaToken] = RateLimitConfig({limit: limit,window: window});...- User
A
still enjoys limit of10M
and revised change is only applicable to new users
Note
Same issue holds for
defaultUserRedemptionLimitConfigs
which new redemption limit is only applied to new usersRecommendation
One possible way could be to add
feeType
to sayRateLimit
which could have values asDEFAULT
orSPECIAL
. IfDEFAULT
then we can verify that current limit for user is same as default user limit, if not we can reset user limit to default limit. This would also require thecapacityUsed
to be reset as per new default limitOndo Finance
Acknowledged. We will be constantly monitoring user states off chain, and it is known that operationally we will need to retroactively edit rate limits in the case of changing defaults.
Frontrunning can be used to disallow Fee Manager operations
Severity
- Severity: Informational
Submitted by
Anurag Jain
Description
User can fail
FEE_MANAGER_ROLE
attempt to change User fee configs by frontrunning. Any simple volume change will causeFEE_MANAGER_ROLE
call to failProof of Concept
- Fee Manager decides to stop giving special fees to a
userID
. - It calls
setUserFeeOverride
withuserFeeOverride[rwaToken][stablecoin][userID].feeConfig.active
asfalse
. - User from impacted
userID
immediately frontrun by subscribing thus changingcurrentVolume
- Fee Manager call fails as volume has changed
if (currentVolume !=userFeeOverride[rwaToken][stablecoin][userID].currentVolume) revert CurrentVolumeIncorrect();Note
Same situation applies for
setUserFeeConfigWindow
andsetUserFee
as wellRecommendation
Important operations such as changing active status should not depend on
currentVolume
changes and should be applied irrespective of any volume changes. This can also be used to remove users who are frontrunning to avoid limit reductionwithdrawToken call should be skipped for 0 token withdrawals
Severity
- Severity: Informational
Submitted by
carrotsmuggler
Description
The
_gatherTokensFromWithdrawalSources
function in the token router contract iterates over the various token sources, checks how much funds are available and then takes that amount out of those sources.The issue is that sometimes there can be 0 funds available in a source. In that case, the contact still tries to take out 0 tokens from that source.
uint256 amountAvailable = tokenSource.availableToWithdraw(outputToken); //@audit can be 0uint256 withdrawAmount = amountAvailable > requestedWithdrawAmount? requestedWithdrawAmount: amountAvailable;// INVARIANT - `TokenSource.withdrawToken` will always withdraw the amount requested// or revert.tokenSource.withdrawToken(outputToken, withdrawAmount); //@audit calling withdrawToken with 0The issue is that some of the token sources can revert on a zero amount withdrawal call.
- If the BUIDL token redemption is paused, the BUIDL USDC source contract might report an available balance of 0. The
withdrawToken
call will then force a call to the redemption contract reverting the transaction. - If the USDS PSM contract is halted, then buy/sell calls to it even with 0 amounts will revert.
Recommendation
If the
withdrawAmount
is 0, skip thetokenSource.withdrawToken
call.ADMIN_SUBSCRIPTION_ROLE can mint during pause
Severity
- Severity: Informational
Submitted by
Anurag Jain
Description
ADMIN_SUBSCRIPTION_ROLE
can still mint token even when contract subscription is in paused state. This could become a problem ifADMIN_SUBSCRIPTION_ROLE
is compromised.In that case, Attacker can use
ADMIN_SUBSCRIPTION_ROLE
to mint tokens viaadminSubscribe
function and sell them in external market (if any). Although impact is limited tomin(globalSubscriptionLimit,adminSubscriptionAllowance[admin])
Recommendation
Add
whenSubscribeNotPaused
toadminSubscribe
function
Gas Optimizations7 findings
Redundant allowance checks
Severity
- Severity: Gas optimization
Submitted by
HickupHH3
Description
Checking the token allowance isn't necessary prior to the invocation of
safeTransferFrom()
, because the transfer should revert if there's insufficient allowance.Recommendation
The allowance checks can be removed.
Redundant InsufficientPsmUsdc check
State
- Acknowledged
Severity
- Severity: Gas optimization
Submitted by
HickupHH3
Description
buyGem()
will revert if there's insufficient USDC inpsm.pocket()
, so the check isn't necessary, though it will provide a clearer revert reason.Proof of Concept
function test_deposit_failInsufficientUsdcWithRealPsm() public {uint256 usdcAvailableBalance = USDC.balanceOf(address(PSM.pocket()));// increase by 1 to exceed available balanceuint256 usdsDepositAmount = (usdcAvailableBalance + 1) * 1e12;deal(address(USDS), depositor, usdsDepositAmount);// Approve recipient to transfer tokensvm.startPrank(depositor);USDS.approve(address(recipient), usdsDepositAmount);recipient.depositToken(address(USDS), usdsDepositAmount);vm.stopPrank();}Recommendation
Consider removing this check.
Ondo Finance
Acknowledged. Gas optimizations generally aren't a concern for our contracts outside of low-hanging fruit. Due to the value of the transactions through these contracts, it is much better to optimize for cleaner code than minor gas savings, and in this case the better error message is more beneficial.
Redundant decrement in SusdsSource.availableToWithdraw()
Severity
- Severity: Gas optimization
Submitted by
HickupHH3
Description
The subtraction by 1 is redundant because
susds.convertToAssets()
rounds down.Proof of Concept
Fuzz testing with some practical values of `susds` found no issues with using the output of `availableToWithdraw()`.function test_fuzzWithdrawMaxAmount(uint256 susdsAmount) public {// bound sudsAmount to be between 1 and 1_000_000e18susdsAmount = bound(susdsAmount, 1, 1_000_000e18);// deal amount to withdrawAddressdeal(address(SUSDS), withdrawAddress, susdsAmount);vm.prank(withdrawAddress);SUSDS.approve(address(source), susdsAmount);vm.startPrank(withdrawer);source.withdrawToken(address(USDS), source.availableToWithdraw(address(USDS)));}Recommendation
- uint256 usdsAvailable = susds.convertToAssets(susdsAvailable);- return usdsAvailable > 0 ? usdsAvailable - 1 : 0;+ return susds.convertToAssets(susdsAvailable);Redundant USD_NORMALIZER normalization
Severity
- Severity: Gas optimization
Submitted by
HickupHH3
Description
The RWA amount calculation includes a multiplication and division by
USD_NORMALIZER
that cancel each other out.Recommendation
rwaAmountOut = (depositUSDValue - fee) * RWA_NORMALIZER) / _getRwaPrice();Redundant uint48 casting of window
Severity
- Severity: Gas optimization
Submitted by
HickupHH3
Description
The
window
parameter is already declared asuint48
in function signatures, but is redundantly cast touint48
when assigning to the struct.Recommendation
Remove the redundant type casting since the parameter is already the correct type:
- window: uint48(window)+ window: windowFirst conditional volume check can include equality case
Severity
- Severity: Gas optimization
Submitted by
HickupHH3
Description
In the case where
userFeeConfig.currentVolume == userFeeConfig.limitVolume
, theeffectiveUSDValue
would simply beusdValue
, which is the first case, so the direct assignment would be desired.Recommendation
- if (userFeeConfig.currentVolume > userFeeConfig.limitVolume) {+ if (userFeeConfig.currentVolume >= userFeeConfig.limitVolume) {currentPrice can be directly assigned to priceStored
Severity
- Severity: Gas optimization
Submitted by
HickupHH3
Context
ContinuousPriceOracle.sol#L243
Description
Since the current price has been synced via
_syncRateAndPriceStoredToNow()
,currentPrice
can simply read from thepriceStored
variable.Recommendation
- uint256 currentPrice = getPrice();+ uint256 currentPrice = priceStored;