Platform: Code4rena
Start Date: 25/07/2023
Pot Size: $80,100 USDC
Total HM: 18
Participants: 7
Period: 10 days
Judge: cccz
Total Solo HM: 15
Id: 268
League: ETH
Rank: 2/7
Findings: 4
Award: $0.00
🌟 Selected for report: 3
🚀 Solo Findings: 2
🌟 Selected for report: bin2chen
Data not available
After shutdown, checkpoints are stopped, leading to possible theft of rewards.
ConvexStakingWrapper
No more checkpoints
after shutdown
, i.e. no updates reward.reward_integral_for[user]
function _beforeTokenTransfer( address _from, address _to, uint256 ) internal override { @> _checkpoint([_from, _to]); } function _checkpoint(address[2] memory _accounts) internal nonReentrant { //if shutdown, no longer checkpoint in case there are problems @> if (isShutdown()) return; uint256 supply = _getTotalSupply(); uint256[2] memory depositedBalance; depositedBalance[0] = _getDepositedBalance(_accounts[0]); depositedBalance[1] = _getDepositedBalance(_accounts[1]); IRewardStaking(convexPool).getReward(address(this), true); _claimExtras(); uint256 rewardCount = rewards.length; for (uint256 i = 0; i < rewardCount; i++) { _calcRewardIntegral(i, _accounts, depositedBalance, supply, false); } }
This would result in, after shutdown
, being able to steal rewards
by transferring tokens
to new users
Example: Suppose the current reward.reward_integral = 1000
When a shutdown
occurs
alice transfers 100 to the new user, bob.
Since bob is the new user and _beforeTokenTransfer()->_checkpoint()
is not actually executed
Result.
balanceOf[bob] = 100
reward.reward_integral_for[bob] = 0
bob executes claimRewards()
to steal the reward
reward amount = balanceOf[bob] * (reward.reward_integral - reward.reward_integral_for[bob])
= 100 * (1000-0)
bob transfers the balance to other new users, looping steps 1-2 and stealing all rewards
Still execute _checkpoint
function _checkpoint(address[2] memory _accounts) internal nonReentrant { //if shutdown, no longer checkpoint in case there are problems - if (isShutdown()) return; uint256 supply = _getTotalSupply(); uint256[2] memory depositedBalance; depositedBalance[0] = _getDepositedBalance(_accounts[0]); depositedBalance[1] = _getDepositedBalance(_accounts[1]); IRewardStaking(convexPool).getReward(address(this), true); _claimExtras(); uint256 rewardCount = rewards.length; for (uint256 i = 0; i < rewardCount; i++) { _calcRewardIntegral(i, _accounts, depositedBalance, supply, false); } }
Context
#0 - c4-judge
2023-08-05T08:44:52Z
thereksfour marked the issue as primary issue
#1 - c4-sponsor
2023-08-30T16:47:38Z
pmckelvy1 marked the issue as sponsor confirmed
#2 - c4-sponsor
2023-08-30T16:47:50Z
pmckelvy1 marked the issue as sponsor acknowledged
#3 - c4-judge
2023-09-05T06:12:09Z
thereksfour marked the issue as satisfactory
#4 - c4-judge
2023-09-05T06:12:15Z
thereksfour marked the issue as selected for report
🌟 Selected for report: ronnyx2017
Also found by: bin2chen
Data not available
curvePool.get_virtual_price()
May be manipulated to cause malicious entry DISABLED
CurveVolatileCollateral._underlyingRefPerTok()
return curvePool.get_virtual_price()
function _underlyingRefPerTok() internal view virtual override returns (uint192) { @> return _safeWrap(curvePool.get_virtual_price()); }
If curvePool
contains ETH
, it can be manipulated by the price.
For more information:
https://chainsecurity.com/curve-lp-oracle-manipulation-post-mortem/
_underlyingRefPerTok()
Being manipulated could lead to entry DISABLED
Check for reentrancy as described in the article.
Since reentrancy locks were not publicly viewable by smart contracts, the reentrancy lock had to be triggered differently— by calling a state-altering function. The criteria for such a function were not to transfer funds from or to oracles and to be relatively cheap in terms of gas overhead. The solution identified most suitable was calling the pools’ withdraw_admin_fees function to trigger the reentrancy lock. Most protocols followed that protection pattern. ## Assessed type Context
#0 - c4-judge
2023-08-05T14:31:08Z
thereksfour marked the issue as primary issue
#1 - tbrent
2023-08-08T20:30:58Z
We have considered this very issue before and have decided as a solution to avoid raw ETH and ERC777's entirely, and not try to detect reentrancy in the way described in the article. This is for a few reasons:
withdraw_admin_fees
is not on all Curve pools, and in particular not on Tricrypto. https://etherscan.io/address/0xd51a44d3fae010294c616388b506acda1bfaae46refresh()
calls means an attacker could gain execution under a different asset's refresh() and use that to manipulate refPerTok().Also related: Our target unit system does not work well with volatile pools. Each pool would need its own target unit and therefore could not be backed up with any other collateral except identically/distributed pools. We plan to remove CurveVolatileCollateral
entirely.
#2 - tbrent
2023-08-08T20:57:02Z
There is documentation on the website indicating to avoid ERC777 tokens, but this should probably be updated to include forbidding LP tokens that contain raw ETH. Though, it is a bit more complicated than that since some LP tokens offer withdrawal functions that automate the unwrapping of WETH into ETH.
https://reserve.org/protocol/rtokens/#non-compatible-erc20-assets
#3 - c4-sponsor
2023-08-30T16:48:53Z
pmckelvy1 marked the issue as sponsor confirmed
#4 - c4-judge
2023-09-05T06:24:22Z
thereksfour marked the issue as satisfactory
#5 - c4-judge
2023-09-05T06:24:26Z
thereksfour marked the issue as selected for report
#6 - 5z1punch
2023-09-05T11:34:15Z
Dup of https://github.com/code-423n4/2023-07-reserve-findings/blob/main/data/ronnyx2017-Q.md#7-curve-read-only-reentrancy-can-increase-the-price-of-some-curvestablecollateral . In fact The price manipulated mentioned in the https://github.com/code-423n4/2023-07-reserve-findings/issues/14 is Curve Read-Only Reentrancy attack. And I explained this attack in more detail in my QA report.
#7 - c4-judge
2023-09-05T12:12:30Z
thereksfour marked the issue as not selected for report
#8 - c4-judge
2023-09-05T12:14:23Z
thereksfour marked the issue as duplicate of #45
🌟 Selected for report: bin2chen
Data not available
transfer missing _updateRewards()
,Resulting in the loss of from
's reward
StaticATokenLM
contains the rewards mechanism, when the balance changes, the global _accRewardsPerToken
needs to be updated first to calculate the user's rewardsAccrued
more accurately.
Example: mint()/burn()
both call _updateRewards()
to update _accRewardsPerToken
function _deposit( address depositor, address recipient, uint256 amount, uint16 referralCode, bool fromUnderlying ) internal returns (uint256) { require(recipient != address(0), StaticATokenErrors.INVALID_RECIPIENT); @> _updateRewards(); ... _mint(recipient, amountToMint); return amountToMint; } function _withdraw( address owner, address recipient, uint256 staticAmount, uint256 dynamicAmount, bool toUnderlying ) internal returns (uint256, uint256) { ... @> _updateRewards(); ... @> _burn(owner, amountToBurn); ... }
When transfer()/transerFrom()
, the balance is also modified, but without calling _updateRewards()
first
The result is that if the user transfers the balance, the difference in rewards accrued by from
is transferred to to
along with it.
This doesn't make sense for from
.
function _beforeTokenTransfer( address from, address to, uint256 ) internal override { if (address(INCENTIVES_CONTROLLER) == address(0)) { return; } if (from != address(0)) { _updateUser(from); } if (to != address(0)) { _updateUser(to); } }
_beforeTokenTransfer
first trigger _updateRewards()
function _beforeTokenTransfer( address from, address to, uint256 ) internal override { if (address(INCENTIVES_CONTROLLER) == address(0)) { return; } + _updateRewards(); if (from != address(0)) { _updateUser(from); } if (to != address(0)) { _updateUser(to); } }
Context
#0 - c4-judge
2023-08-05T14:44:32Z
thereksfour marked the issue as primary issue
#1 - tbrent
2023-08-08T20:32:33Z
We will be reaching out to the Aave team to understand more about this. It seems there are multiple places in StaticATokenLM where reward steps are missing, and there may be reasons why.
#2 - julianmrodri
2023-08-28T14:52:00Z
We will mark this issue as Sponsor Acknowledged. It is true the situation described by the warden and that's the behavior we observe. However we will not be implementing any change in the code (besides adding some comments) for the following reasons:
transfer
operation. It is important to remark that any deposit
or withdraw
done to the contract plus any call to collectRewards..
, and any claim of rewards from the Reserve protocol, would setup the correct balances. So while it is true that transfers may in some cases not transfer rewards we expect this to only be slightly off.To clarify this issue for users we will add a comment to the wrapper contract StaticATokenLM
to clarify the situation with how rewards are handled on transfer.
#3 - c4-sponsor
2023-08-28T16:15:47Z
tbrent marked the issue as sponsor acknowledged
#4 - c4-judge
2023-09-05T06:24:05Z
thereksfour marked the issue as satisfactory
#5 - c4-judge
2023-09-05T06:24:09Z
thereksfour marked the issue as selected for report
🌟 Selected for report: bin2chen
Also found by: carlitox477
Data not available
Incorrect determination of maximum rewards, which may lead to loss of user rewards
_claimRewardsOnBehalf()
For users to retrieve rewards
function _claimRewardsOnBehalf( address onBehalfOf, address receiver, bool forceUpdate ) internal { if (forceUpdate) { _collectAndUpdateRewards(); } uint256 balance = balanceOf(onBehalfOf); uint256 reward = _getClaimableRewards(onBehalfOf, balance, false); uint256 totBal = REWARD_TOKEN.balanceOf(address(this)); @> if (reward > totBal) { @> reward = totBal; @> } if (reward > 0) { @> _unclaimedRewards[onBehalfOf] = 0; _updateUserSnapshotRewardsPerToken(onBehalfOf); REWARD_TOKEN.safeTransfer(receiver, reward); } }
From the code above, we can see that if the contract balance is not enough, it will only use the contract balance and set the unclaimed rewards to 0: _unclaimedRewards[user]=0
.
But using the current contract's balance is inaccurate, REWARD_TOKEN
may still be stored in `INCENTIVES_CONTROLLER
_updateRewards()
and _updateUser()
, are just calculations, they don't transfer REWARD_TOKEN
to the current contract, but _unclaimedRewards[user]
is always accumulating
_updateRewards()
not transferable REWARD_TOKEN
function _updateRewards() internal { ... if (block.number > _lastRewardBlock) { ... address[] memory assets = new address[](1); assets[0] = address(ATOKEN); @> uint256 freshRewards = INCENTIVES_CONTROLLER.getRewardsBalance(assets, address(this)); uint256 lifetimeRewards = _lifetimeRewardsClaimed.add(freshRewards); uint256 rewardsAccrued = lifetimeRewards.sub(_lifetimeRewards).wadToRay(); @> _accRewardsPerToken = _accRewardsPerToken.add( (rewardsAccrued).rayDivNoRounding(supply.wadToRay()) ); _lifetimeRewards = lifetimeRewards; } }
_unclaimedRewards[user]
always accumulatingfunction _updateUser(address user) internal { uint256 balance = balanceOf(user); if (balance > 0) { uint256 pending = _getPendingRewards(user, balance, false); @> _unclaimedRewards[user] = _unclaimedRewards[user].add(pending); } _updateUserSnapshotRewardsPerToken(user); }
This way if _unclaimedRewards(forceUpdate=false)
is executed, it does not trigger the transfer of REWARD_TOKEN
to the current contract.
This makes it possible that _unclaimedRewards[user] > REWARD_TOKEN.balanceOf(address(this))
According to the _claimedRewardsOnBehalf()
current code, the extra value is lost.
It is recommended that if (reward > totBal)
be executed only if forceUpdate=true
, to avoid losing user rewards.
function _claimRewardsOnBehalf( address onBehalfOf, address receiver, bool forceUpdate ) internal { if (forceUpdate) { _collectAndUpdateRewards(); } uint256 balance = balanceOf(onBehalfOf); uint256 reward = _getClaimableRewards(onBehalfOf, balance, false); uint256 totBal = REWARD_TOKEN.balanceOf(address(this)); - if (reward > totBal) { + if (forceUpdate && reward > totBal) { reward = totBal; } if (reward > 0) { _unclaimedRewards[onBehalfOf] = 0; _updateUserSnapshotRewardsPerToken(onBehalfOf); REWARD_TOKEN.safeTransfer(receiver, reward); } }
Context
#0 - c4-judge
2023-08-05T04:22:49Z
thereksfour marked the issue as primary issue
#1 - c4-judge
2023-08-06T16:39:32Z
thereksfour changed the severity to 2 (Med Risk)
#2 - tbrent
2023-08-08T20:41:04Z
#3 - julianmrodri
2023-08-28T13:57:51Z
After a thorough review we can confirm this is not an issue. This is the way it should work and that's the reason why there is a forceUpdate param. When forceUpdate == true, then you will always have the latest rewards to claim and the updated balance.
When is set to false, it will only distribute the rewards that were previously collected (the ones available in the contract). It is correct there might be additional rewards to be collected, but that can easily be done with another call to the same function using the forceUpdate == true.
There are no rewards "lost" in the process, no fix needs to be implemented. Even though unclaimedRewards
is set to zero, then it will be populated with all the pending
rewards again so the amount will be ok.
Moreover, the suggested fix would brick the function most of the time (as usually rewards are bigger than balance because it includes uncollected but pending rewards), and in that case it would attempt to transfer rewards not available in the contract. The check of just sending the balance in those cases is required.
#4 - c4-sponsor
2023-08-28T16:13:52Z
tbrent marked the issue as sponsor disputed
#5 - thereksfour
2023-08-31T05:08:11Z
@bin2chen66 please take a look. It seems that since _getPendingRewards has a false parameter, _unclaimedRewards does not accumulate unclaimed rewards in the controller, so the rewards are not lost.
function _updateUser(address user) internal { uint256 balance = balanceOf(user); if (balance > 0) { uint256 pending = _getPendingRewards(user, balance, false); _unclaimedRewards[user] = _unclaimedRewards[user].add(pending); } _updateUserSnapshotRewardsPerToken(user); }
#6 - bin2chen66
2023-08-31T06:11:00Z
@thereksfour
getPendingRewards(fresh = true)
It doesn't matter if fresh
istrue
or false
, because this can only be used to calculate the latest global accRewardsPerToken
Since _updateRewards()
must be executed before _updateUser (user)
is executed to ensure that accRewardsPerToken
is up-to-date, it does not matter whether fresh
is true
But the message above
Even though unclaimedRewards is set to zero, then it will be populated with all the pending rewards again so the amount will be ok.
It confuses me a bit, I might need to take another look I need to familiarize myself with this project again to see if I missed something
#7 - thereksfour
2023-08-31T06:22:19Z
_getClaimableRewards returns _unclaimedRewards + pendingRewards, that is, reward = _unclaimedRewards + pendingRewards, so just setting _unclaimedRewards to 0 will not decrease pendingRewards, which may be somewhat helpful
uint256 reward = _getClaimableRewards(onBehalfOf, balance, false); uint256 totBal = REWARD_TOKEN.balanceOf(address(this)); if (reward > totBal) { reward = totBal; } if (reward > 0) { _unclaimedRewards[onBehalfOf] = 0; _updateUserSnapshotRewardsPerToken(onBehalfOf); REWARD_TOKEN.safeTransfer(receiver, reward); } ... function _getClaimableRewards( address user, uint256 balance, bool fresh ) internal view returns (uint256) { uint256 reward = _unclaimedRewards[user].add(_getPendingRewards(user, balance, fresh)); return reward.rayToWadNoRounding(); }
#8 - bin2chen66
2023-08-31T06:39:11Z
@thereksfour
pendingRewards
is assumed to be 0.
But _unclaimedRewards[user]
has a value, the point is that the value in there is not in the current contract, it's in INCENTIVES_CONTROLLER
.
If it's cleared, it's gone.
I thank I need to take another look.
#9 - bin2chen66
2023-08-31T08:25:45Z
@thereksfour I'll keep my original point Please help me see if I'm missing something. Thanks
The current implementation only moves rewards to the current contract if _collectAndUpdateRewards() is executed
_updateRewards() and _updateUser() are not triggered.
But _unclaimedRewards[user] is accumulated. _accRewardsPerToken and _userSnapshotRewardsPerToken[user] keeps getting bigger.
so that if no one has called _collectAndUpdateRewards() i.e. forceUpdate=false is not called.
This way the rewards balance in the contract will always be zero.
After _claimRewardsOnBehalf(forceUpdate=false). The user doesn't get any rewards, but _unclaimedRewards[user] is cleared to 0 and can't be refilled (note that it's not pendingRewards, assuming that pendingRewards is 0).
This way the rewards are lost
#10 - thereksfour
2023-08-31T10:29:50Z
Need review from sponsors. @julianmrodri
#11 - bin2chen66
2023-08-31T11:18:30Z
Here's a test case to look at Note:The balance of the current contract described above cannot be 0, it needs to be a little bit
add to StaticATokenLM.test.ts
it('test_lost', async () => { const amountToDeposit = utils.parseEther('5') // Just preparation await waitForTx(await weth.deposit({ value: amountToDeposit.mul(2) })) await waitForTx( await weth.approve(staticAToken.address, amountToDeposit.mul(2), defaultTxParams) ) // Depositing await waitForTx( await staticAToken.deposit(userSigner._address, amountToDeposit, 0, true, defaultTxParams) ) await advanceTime(1); //***** need small reward balace await staticAToken.collectAndUpdateRewards() const staticATokenBalanceFirst = await stkAave.balanceOf(staticAToken.address); await advanceTime(60 * 60 * 24) // Depositing await waitForTx( await staticAToken.deposit(userSigner._address, amountToDeposit, 0, true, defaultTxParams) ) const beforeRewardBalance = await stkAave.balanceOf(userSigner._address); const pendingRewardsBefore = await staticAToken.getClaimableRewards(userSigner._address) console.log("user Reward Balance(Before):",beforeRewardBalance); // user claim forceUpdate = false await waitForTx(await staticAToken.connect(userSigner).claimRewardsToSelf(false)) const afterRewardBalance = await stkAave.balanceOf(userSigner._address); const pendingRewardsAfter = await staticAToken.getClaimableRewards(userSigner._address) console.log("user Reward Balance(After):",afterRewardBalance); const pendingRewardsDecline = pendingRewardsBefore.toNumber() - pendingRewardsAfter.toNumber() ; const getRewards= afterRewardBalance.toNumber() - beforeRewardBalance.toNumber() ; console.log("user pendingRewardsBefore:",pendingRewardsBefore); console.log("user pendingRewardsAfter:",pendingRewardsAfter); const staticATokenBalanceAfter = await stkAave.balanceOf(staticAToken.address); console.log("staticAToken Balance (before):",staticATokenBalanceFirst); console.log("staticAToken Balance (After):",staticATokenBalanceAfter); console.log("user lost:",pendingRewardsDecline - getRewards); })
$ yarn test:plugins:integration --grep "test_lost" StaticATokenLM: aToken wrapper with static balances and liquidity mining Duplicate definition of RewardsClaimed (RewardsClaimed(address,address,uint256), RewardsClaimed(address,address,address,uint256)) Rewards - Small checks user Reward Balance(Before): BigNumber { value: "0" } user Reward Balance(After): BigNumber { value: "34497547939" } user pendingRewardsBefore: BigNumber { value: "1490345817336159" } user pendingRewardsAfter: BigNumber { value: "34497293495" } staticAToken Balance (before): BigNumber { value: "34497547939" } staticAToken Balance (After): BigNumber { value: "0" } user lost: 1490276822494725 ✔ test_lost (947ms) 1 passing (24s)
#12 - julianmrodri
2023-08-31T11:54:30Z
Thanks for the example. I'll review and let you know
#13 - julianmrodri
2023-08-31T13:09:52Z
Ok, the issue exists I can confirm now. This is a tricky one, nice catch. Found out that this was built this way on purpose. The idea is to allow the user to "sacrifice" some rewards for gas savings. You can see it in some of the tests, with comments like this one:
expect(pendingRewards5).to.be.eq(0) // User "sacrifice" excess rewards to save on gas-costs
We will discuss today what action we are taking with this issue.
in any case, the suggested mitigation does not address fully the issue, and causes the contract to fail under normal operations (simply try to run our test suite with that change). I believe we should probably address the main issue that the _unclaimed
variable is set to 0, instead of to the rewards still pending to be collected.
What do you think about the function working this way? I ran a simple check and seems to address it at least for the example you provided. This mitigation was suggested on the other ticket linked here, it had some rounding issues but overall is the same.
function _claimRewardsOnBehalf( address onBehalfOf, address receiver, bool forceUpdate ) internal { if (forceUpdate) { _collectAndUpdateRewards(); } uint256 balance = balanceOf(onBehalfOf); uint256 reward = _getClaimableRewards(onBehalfOf, balance, false); uint256 totBal = REWARD_TOKEN.balanceOf(address(this)); if (reward == 0) { return; } if (reward > totBal) { reward = totBal; _unclaimedRewards[onBehalfOf] -= reward.wadToRay(); } else { _unclaimedRewards[onBehalfOf] = 0; } _updateUserSnapshotRewardsPerToken(onBehalfOf); REWARD_TOKEN.safeTransfer(receiver, reward); }
Thanks for taking a look!
#14 - julianmrodri
2023-08-31T13:50:13Z
@thereksfour @bin2chen66 updated response above. Tagging you to make sure you are notified.
#15 - bin2chen66
2023-08-31T14:30:00Z
@julianmrodri This one still has problems
pendingReward
may underflow
For example:
balance = 10
_unclaimedRewards[user]=9
pendingRewards = 2_unclaimedRewards[onBehalfOf] -= reward.wadToRay();
will underflow
_updateUserSnapshotRewardsPerToken(onBehalfOf);
Then we need to accumulate pendingRewards
to _unclaimedRewards[user]
.Personally, I feel that if we don't want to revert, try this
function _claimRewardsOnBehalf( address onBehalfOf, address receiver, bool forceUpdate ) internal { if (forceUpdate) { _collectAndUpdateRewards(); } uint256 balance = balanceOf(onBehalfOf); uint256 reward = _getClaimableRewards(onBehalfOf, balance, false); uint256 totBal = REWARD_TOKEN.balanceOf(address(this)); if (reward == 0) { return; } if (reward > totBal) { + // Insufficient balance resulting in no transfers out put into _unclaimedRewards[] + _unclaimedRewards[onBehalfOf] = (reward -totBal).wadToRay(); reward = totBal; - _unclaimedRewards[onBehalfOf] -= reward.wadToRay(); } else { _unclaimedRewards[onBehalfOf] = 0; } _updateUserSnapshotRewardsPerToken(onBehalfOf); REWARD_TOKEN.safeTransfer(receiver, reward); }
This may still have this prompt.
expect(pendingRewards5).to.be.eq(0) // User "sacrifice" excess rewards to save on gas-costs
But I feel that this use case should be changed. It is okay to sacrifice a little. If it is a lot, it is still necessary to prevent the user from executing it.
#16 - c4-sponsor
2023-08-31T16:33:11Z
pmckelvy1 (sponsor) acknowledged
#17 - julianmrodri
2023-08-31T16:42:11Z
@thereksfour @bin2chen66 We had a group call and we decided to ACKNOWLEDGE the issue but we will not make code changes, just add a comment in the contract explaining this risk of losing rewards if you call it with the false
parameter.
The reasons are:
true
, and nobody can claim on behalf of the protocol with the false
parameter, the Protocol will always get the latest rewards when claimingHowever, we acknowledge and value the finding which was spot on and allowed us to understand the wrapper in more detail. Thanks for that!
#18 - c4-judge
2023-09-05T06:23:52Z
thereksfour marked the issue as satisfactory
#19 - c4-judge
2023-09-05T06:23:56Z
thereksfour marked the issue as selected for report
🌟 Selected for report: auditor0517
Also found by: 0xA5DF, RaymondFam, bin2chen, carlitox477, ronnyx2017
Data not available
L-01: StaticATokenLM if out-of-gas may result in INCENTIVES_CONTROLLER == address(0)
In the constructor method of StaticATokenLM
, a try/catch is executed
constructor( ILendingPool pool, address aToken, string memory staticATokenName, string memory staticATokenSymbol ) public { ... try IAToken(aToken).getIncentivesController() returns ( IAaveIncentivesController incentivesController ) { if (address(incentivesController) != address(0)) { @> INCENTIVES_CONTROLLER = incentivesController; @> REWARD_TOKEN = IERC20(INCENTIVES_CONTROLLER.REWARD_TOKEN()); } @> } catch {} ASSET = IERC20(IAToken(aToken).UNDERLYING_ASSET_ADDRESS()); ASSET.safeApprove(address(pool), type(uint256).max); }
If try
gets out-of-gas
due to a gas estimation error, but the contract deploys normally due to the 1/64
reserved gas, then the contract will deploy normally.
This may result in the contract deploying and actually having INCENTIVES_CONTROLLER
, but getting INCENTIVES_CONTROLLER==address(0)
.
Suggestion.
If out-of-gas
then revert
constructor( ILendingPool pool, address aToken, string memory staticATokenName, string memory staticATokenSymbol ) public { ... try IAToken(aToken).getIncentivesController() returns ( IAaveIncentivesController incentivesController ) { if (address(incentivesController) != address(0)) { INCENTIVES_CONTROLLER = incentivesController; REWARD_TOKEN = IERC20(INCENTIVES_CONTROLLER.REWARD_TOKEN()); } - } catch {} + } catch (bytes memory errData) { + if (errData.length == 0) revert(); // out-of-gas + } ASSET = IERC20(IAToken(aToken).UNDERLYING_ASSET_ADDRESS()); ASSET.safeApprove(address(pool), type(uint256).max);
L-02: MorphoFiatCollateral._underlyingRefPerTok() ERC4626 first user inflation attack
MorphoFiatCollateral._underlyingRefPerTok()
using convertToAssets()
as _underlyingRefPerTok()
First user executable, common ERC4626
First user inflation attack
asset
, raising totalAssets()
_underlyingRefPerTok()
Formula: shares.mulDiv(totalAssets() + 1, totalSupply() + 10**_decimalsOffset(), rounding);
_underlyingRefPerTok()
= 1e18 * totalAssets() / 1ERC4626.deposit()
and ERC4626.draw()
to retrieve all assets
.to get the normal
_underlyingRefPerTok()`, but lower than the value in step 2DISABLED
.Recommendation. Deploy with pre-stored shares
L-03: StargatePoolFiatCollateral missing revenueHiding
mechanisms
Currently StargatePoolFiatCollateral
does not have a revenueHiding
mechanism similar to AppreciatingFiatCollateral
to retaliate against minor revenue drops (e.g. caused by loss of precision).
It is recommended to inherit directly from AppreciatingFiatCollateral.sol
, do not override refresh
, and directly use AppreciatingFiatCollateral.sol
's refersh() method.
L-04: MorphoTokenisedDeposit override _decimalsOffset() ==0 increase ERC4626 inflation attack
risk
MorphoTokenisedDeposit._decimalsOffset
override RewardableERC4626Vault._decimalsOffset()
and return 0
function _decimalsOffset() internal view virtual override returns (uint8) { return 0; }
It is recommended not to override, but to continue to use RewardableERC4626Vault._decimalsOffset() ==9
L-05: MorphoTokenisedDeposit._deposit() It is not recommended to use safeApprove, it is recommended to use the safeIncreaseAllowance()
SafeERC20.safeApprove()
Determines if the current allowance is equal to 0, applies to the first approve
, since _deposit()
will be used repeatedly it is recommended to use safeIncreaseAllowance()
.
to avoid the risk of revert if the allowance is not used up.
/** * @dev Deprecated. This function has issues similar to the ones found in * {IERC20-approve}, and its usage is discouraged. * * Whenever possible, use {safeIncreaseAllowance} and * {safeDecreaseAllowance} instead. */ function safeApprove( IERC20 token, address spender, uint256 value ) internal { // safeApprove should only be called when setting an initial allowance, // or when resetting it to zero. To increase and decrease it, use // 'safeIncreaseAllowance' and 'safeDecreaseAllowance' require( @> (value == 0) || (token.allowance(address(this), spender) == 0), "SafeERC20: approve from non-zero to non-zero allowance" ); _callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, value)); }
#0 - c4-judge
2023-08-06T10:17:49Z
thereksfour marked the issue as grade-a