Platform: Code4rena
Start Date: 15/06/2023
Pot Size: $79,800 USDC
Total HM: 14
Participants: 6
Period: 14 days
Judge: 0xean
Total Solo HM: 11
Id: 248
League: ETH
Rank: 1/6
Findings: 7
Award: $0.00
π Selected for report: 7
π Solo Findings: 4
π Selected for report: 0xA5DF
Data not available
quoteCustomRedemption()
works under the assumption that the maximum size of the erc20sAll
should be assetRegistry.size()
, however there can be cases where an asset was unregistered but still exists in an old basket, making the size of the old basket greater than assetRegistry.size()
. In that case the function will revert with an index out of bounds error.
Users might not be able to use redeemCustom
when needed.
I think this should be considered high severity, since being able to redeem the token at all time is an essential feature for the protocol that's allowed also while frozen. Not being able to redeem can result in a depeg or in governance becoming malicious and stealing RToken collateral.
Consider the following scenario:
As for the revert:
erc20sAll
is created here with the length of assetRegistry.size()
, which is 2 in our case.erc20sAll
which will result in an index-out-of-bonds error(the function doesn't include in the final results assets that aren't registered, but it does push them too into erc20sAll
)
Allow the user to specify the length of the array erc20sAll
to avoid this revert
Other
#0 - 0xean
2023-06-11T18:07:57Z
I believe this to be a stretch for high severity. It has several pre-conditions to end up in the proposed state and I do believe it would be entirely possible for governance to change back to the original state (USDC, USDT, DAI), so assets wouldn't be lost and the impact would more be along the lines of a temporary denial of service.
Look forward to warden and sponsor comments.
#1 - tbrent
2023-06-12T19:28:36Z
@0xA5DF nice find! Thoughts on an alternative mitigation?
erc20sAll
never includes unregistered ERC20sassetsAll
for re-use around L438#2 - 0xA5DF
2023-06-12T19:54:53Z
Hey @tbrent
That can work as well, the only downside I can think of is that in case there's an asset that's not registered and is repeated across different baskets - the toAsset()
would be called multiple times for that asset (while under the current implementation and under the mitigation I've suggested it'll be called only once), this would cost about 300 gas units per additional call (100 for the call, 2 sload
s to a warm slot inside the call itself)
#3 - tbrent
2023-06-12T20:12:29Z
Hey @tbrent That can work as well, the only downside I can think of is that in case there's an asset that's not registered and is repeated across different baskets - the
toAsset()
would be called multiple times for that asset (while under the current implementation and under the mitigation I've suggested it'll be called only once), this would cost about 300 gas units per additional call (100 for the call, 2sload
s to a warm slot inside the call itself)
Noted, good point.
#4 - c4-sponsor
2023-07-04T23:21:58Z
tbrent marked the issue as sponsor confirmed
#5 - 0xean
2023-07-12T17:41:31Z
@tbrent - do you care to comment on your thoughts on severity? I am leaning towards M on this, but it sounds like you believe it is correct as labeled (high).
#6 - tbrent
2023-07-12T18:04:28Z
@tbrent - do you care to comment on your thoughts on severity? I am leaning towards M on this, but it sounds like you believe it is correct as labeled (high).
Correct, I think high is appropriate.
#7 - c4-judge
2023-07-12T19:18:15Z
0xean marked the issue as satisfactory
π Selected for report: 0xA5DF
Data not available
https://github.com/reserve-protocol/protocol/blob/c4ec2473bbcb4831d62af55d275368e73e16b984/contracts/p1/StRSR.sol#L441-L444 https://github.com/reserve-protocol/protocol/blob/c4ec2473bbcb4831d62af55d275368e73e16b984/contracts/p1/StRSR.sol#L457-L460
When RSR seizure occurs the staking and drafting rate is adjusted accordingly, if any of those rates is above some threshold then a new era begins (draft or staking era accordingly), wiping out all of the holdings of the current era. The assumption is that if the rate is above the threshold then there's not much staking or drafts left after the seizure (and therefore it makes sense to begin a new era). However, there might be a case where a previous seizure has increased the staking/draft rate close to the threshold, and then even a small seizure would make it cross this threshold. In that case the total value of staking or drafts can be very high, and they will all be wiped out by starting a new era.
Stakers will lose their holdings or pending drafts.
Consider the following scenario:
This means the stakings were wiped out despite holding a significant amount of value, causing a loss for the holders.
This one is a bit difficult to mitigate. One way I can think of is to add a 'migration' feature, where in such cases a new era would be created but users would be able to transfer the funds that they held in the previous era into the new era. But this would require some significant code changes and checking that this doesn't break anything or introduces new bugs.
Other
#0 - tbrent
2023-06-12T23:55:26Z
@0xA5DF thoughts on a governance function that requires the ratio be out of bounds, that does beginEra()
and/or beginDraftEra()
?
The idea is that stakers can mostly withdraw, and since governance thresholds are all percentage, vote to immolate themselves and re-start the staking pool. I think it should treat beginEra()
and beginDraftEra()
separately, but I'm not confident in that yet.
#1 - tbrent
2023-07-04T23:22:39Z
We're still not sure how to mitigate this one. Agree it should be considered HIGH and a new issue.
#2 - c4-sponsor
2023-07-04T23:22:45Z
tbrent marked the issue as sponsor acknowledged
#3 - c4-judge
2023-07-12T17:42:00Z
0xean marked the issue as satisfactory
π Selected for report: ronnyx2017
Also found by: 0xA5DF, rvierdiiev
Data not available
This one was reported in the first contest, it was mitigated but a code change that was made since then brings it back again.
Users who stake while frozen would get a share of the rewards for the period since the last call to payoutRewards()
.
This means that in a case of a long freeze (e.g. a week or a few weeks) - users who stake right before the freeze ends would get a reward as if they've staked before the freeze started. This comes at the expense of the stakers who have staked before the freezing begun.
While frozen the _payoutRewards()
isn't called:
function stake(uint256 rsrAmount) public { require(rsrAmount > 0, "Cannot stake zero"); if (!main.frozen()) _payoutRewards();
Not paying out the rewards meaning that the next time _payoutRewards()
would be called the stakers would get a reward as if they've staked at the last time it was called.
Consider paying out rewards also while frozen
Other
#0 - 0xA5DF
2023-06-12T08:17:36Z
This and #11 are dupes too @0xean
#1 - c4-judge
2023-06-12T12:54:27Z
0xean marked the issue as primary issue
#2 - c4-sponsor
2023-07-04T22:43:57Z
tbrent marked the issue as sponsor confirmed
#3 - c4-judge
2023-07-12T19:35:34Z
0xean marked the issue as satisfactory
#4 - c4-judge
2023-07-15T13:06:47Z
0xean marked issue #11 as primary and marked this issue as a duplicate of 11
π Selected for report: ronnyx2017
Also found by: 0xA5DF, rvierdiiev
Data not available
The new cancelUnstake()
function allows users to cancel their unstaking, by taking the user's drafts and minting it again.
However, since the _payoutRewards()
isn't being called this means that the user would get rewards for the period between the last time _payoutRewards()
was called and the time of cancellation.
Users would get rewards they don't deserve, at the expense of other stakers.
cancelUnstake()
doesn't call _payoutRewards()
, this means that the minting part would be done with the old staking rate.
Using an old staking rate means that the user would get a share of the rewards since the last time _payoutRewards()
was called.
Call _payoutRewards()
before the minting
Other
#0 - c4-judge
2023-06-11T20:50:42Z
0xean marked the issue as duplicate of #10
#1 - c4-judge
2023-07-12T20:03:12Z
0xean marked the issue as satisfactory
π Selected for report: 0xA5DF
Data not available
During a Dutch Auction, if a user places a bid, the trade is settled in the same transaction. As part of this process, the backing manager tries to call the rebalance()
function again.
The call to rebalance()
is wrapped in a try-catch block, if an error occurs and the error data is empty, the function will revert.
The assumption is that if the error data is empty that means it was due to an out-of-gas error, this assumption isn't always true as mentioned in a previous issue (that wasn't mitigated). In the case of this issue, this can result in a case where users can't bid on an auction for some time, ending up selling an asset for a price lower than the market price.
Protocol's assets will be auctioned for a price lower than the market price
Consider the following scenario:
On top of checking that the error data is empty, compare the gas before and after to ensure this is an out-of-gas error.
Error
#0 - 0xean
2023-06-11T20:45:55Z
On the fence on this one, it is based off a known issue from a previous contest but does show a new problem stemming from the same problem of oracle deprecation.
Look forward to sponsor comment.
#1 - tbrent
2023-06-13T19:37:17Z
The PoC does not function as specified. Specifically, bidding on an auction does not involve the price at the time of the tx. The price is set at the beginning of the dutch auction in the init()
function. Therefore, it is the starting of new auctions that will revert while the oracle is deprecated, while bids will succeed and simply fail to start the next auction.
#2 - c4-sponsor
2023-07-04T23:14:17Z
tbrent marked the issue as sponsor disputed
#3 - c4-judge
2023-07-12T19:52:40Z
0xean marked the issue as unsatisfactory: Invalid
#4 - 0xA5DF
2023-07-13T08:15:54Z
Therefore, it is the starting of new auctions that will revert while the oracle is deprecated, while bids will succeed and simply fail to start the next auction.
Hey @tbrent
Didn't quite understand the dispute here, if starting the next auction will fail/revert then the bid will revert too.
bid()
calls origin.settleTrade()
and settleTrade()
calls rebalance()
If rebalance()
reverts due to a deprecated oracle then settleTrade()
will revert too (rebalance()
will revert with empty data, and therefore the catch block will trigger a revert here)
#5 - c4-sponsor
2023-07-14T17:43:33Z
tbrent marked the issue as sponsor confirmed
#6 - tbrent
2023-07-14T17:43:40Z
Ah, understood now. Agree this is Medium and think it should be counted as a new finding since the consequence (dutch auction economics break) is novel.
#7 - c4-judge
2023-07-14T18:13:07Z
0xean marked the issue as satisfactory
#8 - tbrent
2023-08-02T17:36:09Z
Hey @0xa5df -- we're having some confusion around exactly what happens when a chainlink oracle is deprecated. Do you have details to share about what this ends up looking like?
We're having trouble finding documentation on this, and it feels like the aggregator contract should just stay there and return a stale value. Is that not right? Has this happened in the past or has Chainlink committed to a particular approach for deprecating?
#9 - 0xA5DF
2023-08-02T19:45:53Z
Hey
It's a bit difficult to track deprecated Chainlink oracles since Chainlink removes the announcement once they're deprecated.
I was able to track one Oracle that was deprecated during the first contest, from the original issue this seems to be this one.
It seems that what happens is that Chainlink sets the aggregator address to the zero address, which makes the call to latestRoundData()
to revert without any data (I guess this is due to the way Solidity handles calls to a non-contract address).
See also the PoC in the original issue in the January contest
#10 - tbrent
2023-08-02T20:42:18Z
Got it, checks out. Thanks!
π Selected for report: 0xA5DF
Also found by: rvierdiiev
Data not available
https://github.com/reserve-protocol/protocol/blob/c4ec2473bbcb4831d62af55d275368e73e16b984/contracts/p1/AssetRegistry.sol#L89-L93 https://github.com/reserve-protocol/protocol/blob/c4ec2473bbcb4831d62af55d275368e73e16b984/contracts/p1/AssetRegistry.sol#L106-L110
At the mitigation contest there was an issue regarding the basketHandler.quantity()
call at the unregistration process taking up all gas.
As a mitigation to that issue the devs set aside some gas and use the remaining to do that call.
This opens up to a new kind of attack, where a attacker can cause the call to revert by not supplying enough gas to it.
This can cause the basket to get disabled, which would require a basket refresh. After a basket refresh is done, an additional warmup period has to pass for some functionality to be available again (issuance, rebalancing and forwarding revenue). In some cases this might trigger a basket switch that would require the protocol to rebalance via trading, trading can have some slippage which can cause a loss for the protocol.
The quantity()
function is being called with the amount of gas that _reserveGas()
returns
If an attacker causes the gas to be just right above GAS_TO_RESERVE
the function would be called with 1 unit of gas, causing it to revert:
function _reserveGas() private view returns (uint256) { uint256 gas = gasleft(); require(gas > GAS_TO_RESERVE, "not enough gas to unregister safely"); return gas - GAS_TO_RESERVE; }
Regarding the unnecessary trade, consider the following scenario:
The refresh was unnecessary and therefore the trade too.
Reserve gas for the call as well:
function _reserveGas() private view returns (uint256) { uint256 gas = gasleft(); - require(gas > GAS_TO_RESERVE, "not enough gas to unregister safely"); + require(gas >= GAS_TO_RESERVE + MIN_GAS_FOR_EXECUTION, "not enough gas to unregister safely"); return gas - GAS_TO_RESERVE; }
Disclosure: this issue was mentioned in the comments to the issue in the mitigation contest, however since this wasn't noticed by the devs and isn't part of the submission I don't think this should be considered a known issue
Other
#0 - 0xean
2023-06-11T20:49:31Z
Applaud @0xA5DF for highlighting this on their own issue.
Disclosure: this issue was https://github.com/code-423n4/2023-02-reserve-mitigation-contest-findings/issues/73#issuecomment-1435483929 to the issue in the mitigation contest, however since this wasn't noticed by the devs and isn't part of the submission I don't think this should be considered a known issue
Look forward to discussion with sponsor.
#1 - tbrent
2023-06-13T19:33:13Z
We've discussed and agree with with the warden that this should not be considered a known issue.
#2 - c4-sponsor
2023-07-04T23:14:35Z
tbrent marked the issue as sponsor confirmed
#3 - c4-judge
2023-07-12T19:29:28Z
0xean marked the issue as primary issue
#4 - c4-judge
2023-07-12T19:52:55Z
0xean marked the issue as satisfactory
#5 - c4-judge
2023-07-15T13:07:58Z
0xean marked the issue as selected for report
π Selected for report: 0xA5DF
Data not available
https://github.com/reserve-protocol/protocol/blob/c4ec2473bbcb4831d62af55d275368e73e16b984/contracts/p1/RToken.sol#L245-L338 https://github.com/reserve-protocol/protocol/blob/c4ec2473bbcb4831d62af55d275368e73e16b984/contracts/p1/BasketHandler.sol#L437-L440
Custom redemption allows to redeem RToken in exchange of a mix of previous baskets (as long as it's not more than the prorata share of the redeemer). The assumption is that previous baskets aren't worth more than the target value of the basket. However, a previous basket can contain a collateral that depegged upwards and is now actually worth more than its target.
Funds that are supposed to go revenue traders would be taken by an attacker redeeming RToken
The following code shows that when a depeg occurs the collateral becomes IFFY (which means it'll be disabled after a certain delay):
// If the price is below the default-threshold price, default eventually // uint192(+/-) is the same as Fix.plus/minus if (pegPrice < pegBottom || pegPrice > pegTop || low == 0) { markStatus(CollateralStatus.IFFY); } else { markStatus(CollateralStatus.SOUND); }
Consider the following scenario:
delayUntilDefault
passes the basket gets disabledWhen doing custom redemption check that the collateral used is sound or at least check that the price isn't higher then the peg price.
Other
#0 - tbrent
2023-07-04T23:17:39Z
This is also true of normal redemption via redeem()
until refreshBasket()
is called after the collateral has cycled from IFFY to DISABLED.
It only checks basketHandler.fullyCollateralized()
, not basketHandler.isReady()
. This is intended. It is important to not disallow redemption, and using USD prices to determine redemption quantities is opposite to the fundamental design of the protocol.
#1 - c4-sponsor
2023-07-04T23:17:44Z
tbrent marked the issue as sponsor disputed
#2 - c4-sponsor
2023-07-04T23:18:12Z
tbrent marked the issue as sponsor acknowledged
#3 - c4-sponsor
2023-07-04T23:18:16Z
tbrent marked the issue as disagree with severity
#4 - tbrent
2023-07-04T23:21:43Z
Agree that the behavior is as the warden indicates. All alternatives seem worse, however. I think this is probably not HIGH. We do not expect to make any change to the behavior.
#5 - 0xean
2023-07-12T17:39:47Z
I think M seems like the correct severity here. There are pre-conditions for this to occur and in addition the likelihood doesn't seem very high
#6 - c4-judge
2023-07-12T17:39:52Z
0xean changed the severity to 2 (Med Risk)
#7 - c4-judge
2023-07-12T17:39:58Z
0xean marked the issue as satisfactory
π Selected for report: 0xA5DF
Also found by: RaymondFam, carlitox477, hihen, ronnyx2017, rvierdiiev
Data not available
Before redemption furnace.melt()
is called, which increases a bit the amount of assets the redeemer gets in return.
While frozen the melt()
call will revert, but since the call is surrounded by a try-catch block the redemption will continue.
This can lead to a case where a user sends out a redeem tx, expecting melt to be called by the redeem function, but before the tx is executed the protocol gets frozen. This means the user wouldn't get the additional value they expected to get from melting (and that they can get if they choose to wait till after the freeze is over).
If the last time melt()
was called wasn't long enough then the additional value from melting wouldn't be that significant. But there can be cases where it can be longer - e.g. low activity or after a freeze (meaning there are 2 freezes one after the other and a user is attempting to redeem between them).
As a mitigation allow the user to specify if they're ok with skipping the call to melt()
, if they didn't specifically allow it then revert.
leakyRefresh()
keeps track of the percentage that was withdrawn since last refresh by adding up the current percentage that's being withdrawn each time.
However, the current percentage is being calculated as part of the current totalRSR
, which doesn't account for the already withdrawn drafts.
This will trigger the withdrawalLeak
threshold earlier than expected.
E.g. if the threshold is set to 25% and 26 users try to withdraw 1% each - the leaky refresh would be triggered by the 23rd person rather than by the 26th.
Reorg attacks aren't very common on mainnet (but more common on L2s if the protocol intends to ever launch on them), but they can still happen (there was a 7 blocks reorg on the Beacon chain before the merge). It can be relevant in the following cases (I've reported a med separately, the followings are the ones I consider low):
EasyAuction
contract which is OOS)As a mitigation - make sure to deploy all contracts with create2
using a salt that's unique to the features of the contract, that will ensure that even in the case of a reorg it wouldn't be deployed to the same address as before.
distributeTokenToBuy()
can be called while paused/frozenDue to the removal of notPausedOrFrozen
from Distributor.distribute()
it's now possible to execute RevenueTrader.distributeTokenToBuy()
while paused or frozen.
This is relevant when tokensToBuy
is sent directly to the revenue trader: RSR sent to RSRTrader or RToken sent directly to RTokenTrader
redeemCustom
allows the use of the zero basketThe basket with nonce #0 is an empty basket, redeemCustom
allows to specify that basket for redemption, which will result in a loss for the redeemer.
refreshBasket
can be called before the first prime basket was setThis will result in an event being emitted but will not impact the contract's state.
MIN_AUCTION_LENGTH
seems too lowThe current MIN_AUCTION_LENGTH
is set to 2 blocks.
This seems a bit too low since the price is time-dependant that means there would be only 3 price options for the auctions, and the final price wouldn't necessarily be the optimal price for the protocol.
This isnβt very likely to happen (currently the only token that yields RSR is StRSR of another RToken) but itβs worth keeping an eye on it.
Consider the following scenario:
This might be intended design, but worth taking this scenario into account
#0 - c4-judge
2023-06-11T21:00:54Z
0xean marked the issue as grade-a
#1 - c4-judge
2023-06-11T21:00:58Z
0xean marked the issue as selected for report
#2 - liveactionllama
2023-07-12T23:09:46Z
Per conversation with judge @0xean - all items in this submission are valid.
π Selected for report: 0xA5DF
Also found by: RaymondFam, carlitox477, hihen
Data not available
toColl()
and toAsset()
use the mapping to check if asset existsSavings: ~2.1K per call Notice this is a function that's being called frequently, and many times per tx. Overall this can save a few thousands of gas units per tx for the most common txs (e.g. issuance, redemption)
At toColl()
and toAsset()
instead of using the EnumberableSet to check that the erc20 is registered just check that the value returned from the mapping isn't zero (this is supposed to be equivalent as long as the governance doesn't register the zero address as an asset contract - maybe add a check for that at register()
).
Proposed changes:
/// Return the Asset registered for erc20; revert if erc20 is not registered. // checks: erc20 in assets // returns: assets[erc20] function toAsset(IERC20 erc20) external view returns (IAsset) { IAsset asset = assets[erc20]; require(asset != IAsset(address(0)), "erc20 unregistered"); return asset; } /// Return the Collateral registered for erc20; revert if erc20 is not registered as Collateral // checks: erc20 in assets, assets[erc20].isCollateral() // returns: assets[erc20] function toColl(IERC20 erc20) external view returns (ICollateral) { IAsset coll = assets[erc20]; require(coll != IAsset(address(0)), "erc20 unregistered"); require(coll.isCollateral(), "erc20 is not collateral"); return ICollateral(address(coll)); }
targetIndex
from mapping instead of iteratingGas savings: a few thousands (see below)
The following code is used at the BasketLib to find the index of a value inside an EnumerableSet
for (targetIndex = 0; targetIndex < targetsLength; ++targetIndex) { if (targetNames.at(targetIndex) == config.targetNames[config.erc20s[i]]) break; }
However the index can be fetched directly from the _indexed
mapping:
diff --git a/contracts/p1/mixins/BasketLib.sol b/contracts/p1/mixins/BasketLib.sol index bc52d1c6..ce56c715 100644 --- a/contracts/p1/mixins/BasketLib.sol +++ b/contracts/p1/mixins/BasketLib.sol @@ -192,10 +192,8 @@ library BasketLibP1 { // For each prime collateral token: for (uint256 i = 0; i < config.erc20s.length; ++i) { // Find collateral's targetName index - uint256 targetIndex; - for (targetIndex = 0; targetIndex < targetsLength; ++targetIndex) { - if (targetNames.at(targetIndex) == config.targetNames[config.erc20s[i]]) break; - } + uint256 targetIndex = targetNames._inner._indexes[config.targetNames[config.erc20s[i]]] -1 ; + assert(targetIndex < targetsLength); // now, targetNames[targetIndex] == config.targetNames[erc20]
Gas savings:
_indexes
keys are considered warm since all values were inserted in the current tx(config.erc20s.length)*(targetsLength-1)/2*100
10*4/2*100=2K
furnace
instead of main.furnace()
Gas savings: ~2.6K
Code: https://github.com/reserve-protocol/protocol/blob/c4ec2473bbcb4831d62af55d275368e73e16b984/contracts/p1/RToken.sol#L184 https://github.com/reserve-protocol/protocol/blob/c4ec2473bbcb4831d62af55d275368e73e16b984/contracts/p1/RToken.sol#L257
At RToken.redeemTo()
and redeemCustom()
furnace is being called using main.furnace()
instead of using the furnace
variable.
The call to main.furnace()
costs both the cold storage variable read at main.furnace()
and an external call to a cold address while using the furnace
variable of the current contract costs only the cold storage read.
The additional cold call would cost ~2.6K
To my understanding both of the values should be equal at all times so there shouldn't be an issue with the replacement.
Gas saved: ~28K per RToken deployment
The struct itself canβt be immutable, but you can save the values of the fields (and fields of the components
) as immutable variables, and use an internal function to build the struct out of those immutable variables
This would save ~2.1K per field, with 13 fields that brings us to ~28K of units saved
lastWithdrawRefresh
only if it has changedGas saved: ~100
At leakyRefresh()
lastWithdrawRefresh
gets updated even if didn't change, that costs an additional 100 gas units.
Proposed change:
- leaked = lastWithdrawRefresh != lastRefresh ? withdrawal : leaked + withdrawal; - lastWithdrawRefresh = lastRefresh; + if(lastWithdrawRefresh != lastRefresh){ + leaked = withdrawal; + lastWithdrawRefresh = lastRefresh; + } else{ + leaked = leaked + withdrawal; + }
sortedAndAllUnique
at BackingManager.forwardRevenue()
Estimated savings: ~n^2*10 where n is the length of the asset. For example for 20 assets that would save ~4K
This is one of the most common ways to save a nice amount of gas. Every additional read costs 100 gas units (when it comes to mapping or arrays there's additional cost), and each additional function call costs at least 100 gas units (usually much more). I've noticed a few instances where a storage variable read or a view-function call can be cached to memory to save gas, I'm pretty sure there are many more instances that I didn't notice.
BasketLib.nextBasket()
refactoringGas saved: a few thousands
The following refactoring saves a few thousands of gas mostly by preventing:
goodCollateral
backup.erc20s
arraydiff --git a/contracts/p1/mixins/BasketLib.sol b/contracts/p1/mixins/BasketLib.sol index bc52d1c6..7ab9c48b 100644 --- a/contracts/p1/mixins/BasketLib.sol +++ b/contracts/p1/mixins/BasketLib.sol @@ -192,10 +192,8 @@ library BasketLibP1 { // For each prime collateral token: for (uint256 i = 0; i < config.erc20s.length; ++i) { // Find collateral's targetName index - uint256 targetIndex; - for (targetIndex = 0; targetIndex < targetsLength; ++targetIndex) { - if (targetNames.at(targetIndex) == config.targetNames[config.erc20s[i]]) break; - } + uint256 targetIndex = targetNames._inner._indexes[config.targetNames[config.erc20s[i]]] -1 ; + assert(targetIndex < targetsLength); // now, targetNames[targetIndex] == config.targetNames[erc20] @@ -244,32 +242,32 @@ library BasketLibP1 { uint256 size = 0; // backup basket size BackupConfig storage backup = config.backups[targetNames.at(i)]; + IERC20[] memory backupsToUse = new IERC20[](backup.erc20s.length); + // Find the backup basket size: min(backup.max, # of good backup collateral) for (uint256 j = 0; j < backup.erc20s.length && size < backup.max; ++j) { - if (goodCollateral(targetNames.at(i), backup.erc20s[j], assetRegistry)) size++; + if (goodCollateral(targetNames.at(i), backup.erc20s[j], assetRegistry)) + { + backupsToUse[size] = backup.erc20s[j]; + size++; + } } // Now, size = len(backups(tgt)). If empty, fail. if (size == 0) return false; - // Set backup basket weights... - uint256 assigned = 0; // Loop: for erc20 in backups(tgt)... - for (uint256 j = 0; j < backup.erc20s.length && assigned < size; ++j) { - if (goodCollateral(targetNames.at(i), backup.erc20s[j], assetRegistry)) { - // Across this .add(), targetWeight(newBasket',erc20) - // = targetWeight(newBasket,erc20) + unsoundPrimeWt(tgt) / len(backups(tgt)) - newBasket.add( - backup.erc20s[j], - totalWeights[i].minus(goodWeights[i]).div( - // this div is safe: targetPerRef > 0: goodCollateral check - assetRegistry.toColl(backup.erc20s[j]).targetPerRef().mulu(size), - CEIL - ) - ); - assigned++; - } + for (uint256 j = 0; j < size; ++j) { + + newBasket.add( + backupsToUse[j], + totalWeights[i].minus(goodWeights[i]).div( + // this div is safe: targetPerRef > 0: goodCollateral check + assetRegistry.toColl(backupsToUse[j]).targetPerRef().mulu(size), + CEIL + ) + ); } // Here, targetWeight(newBasket, e) = primeWt(e) + backupWt(e) for all e targeting tgt }
BasketLib.nextBasket()
cachingOn top of the above refactoring:
config.erc20s[i]
is being read a few times hereconfig.erc20s.length
and backup.erc20s.length
can be cachedtargetNames.at(i)
is being read twice in the second loop (3 before the proposed refactoring)sellAmount
at DutchTrade.settle()
sellAmount
is read here twice if greater than sellBal
soldAmt = sellAmount > sellBal ? sellAmount - sellBal : 0;
quoteCustomRedemption()
loopnonce
Savings: 100*basketNonces.length
here nonce is being read each iteration.
It can be cached outside of the loop
basketNonces.length
Savings: 100*basketNonces.length
b.erc20s.length
Savings: 100*(sum of b.erc20s.length
for all baskets)
This saves gas both for deployment and in case that the revert is triggered.
#0 - c4-judge
2023-06-11T20:56:05Z
0xean marked the issue as selected for report
#1 - c4-judge
2023-06-11T20:59:18Z
0xean marked the issue as grade-a