Platform: Code4rena
Start Date: 24/10/2023
Pot Size: $36,500 USDC
Total HM: 4
Participants: 147
Period: 6 days
Judge: 0xDjango
Id: 299
League: ETH
Rank: 30/147
Findings: 3
Award: $172.78
🌟 Selected for report: 0
🚀 Solo Findings: 0
161.7958 USDC - $161.80
https://github.com/code-423n4/2023-10-ethena/blob/main/contracts/StakedUSDe.sol#L232-L234
The documentation of ethena
protocol states that the addresses with the SOFT_RESTRICTED_STAKER_ROLE
can not withdraw stUSDe for USDe as shown below:
They cannot deposit USDe to get stUSDe or withdraw stUSDe for USDe
But the above restriction is not imposed in the StakedUSDe._withdraw
function on the caller
and _owner
addresses. The input address validation of the StakedUSDe._withdraw
function is shown below:
if (hasRole(FULL_RESTRICTED_STAKER_ROLE, caller) || hasRole(FULL_RESTRICTED_STAKER_ROLE, receiver)) { revert OperationNotAllowed(); }
As it is evident from the above code snippet the StakedUSDe._withdraw
does not verify whether the caller
address or the owner
address does not have the SOFT_RESTRICTED_STAKER_ROLE
during the execution of the StakedUSDe._withdraw
.
if (hasRole(FULL_RESTRICTED_STAKER_ROLE, caller) || hasRole(FULL_RESTRICTED_STAKER_ROLE, receiver)) { revert OperationNotAllowed(); }
https://github.com/code-423n4/2023-10-ethena/blob/main/contracts/StakedUSDe.sol#L232-L234
Manual Review and VSCode
Hence it is recommended to add the following checks on the caller
and _owner
addresses (in the StakedUSDe._withdraw
function) to ensure those addresses do not have the SOFT_RESTRICTED_STAKER_ROLE
so that the stUSDe
can be withdrawn correctly as the documentation states.
if (hasRole(SOFT_RESTRICTED_STAKER_ROLE, caller) || hasRole(SOFT_RESTRICTED_STAKER_ROLE, _owner)) { revert OperationNotAllowed(); }
Invalid Validation
#0 - c4-pre-sort
2023-11-01T02:28:04Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2023-11-01T02:28:15Z
raymondfam marked the issue as duplicate of #52
#2 - c4-judge
2023-11-10T21:44:36Z
fatherGoose1 marked the issue as satisfactory
🌟 Selected for report: 0xmystery
Also found by: 0x11singh99, 0xAadi, 0xAlix2, 0xG0P1, 0xStalin, 0xWaitress, 0x_Scar, 0xhacksmithh, 0xhunter, 0xpiken, Al-Qa-qa, Arz, Avci, Bauchibred, BeliSesir, Breeje, Bughunter101, DarkTower, Eeyore, Fitro, HChang26, Imlazy0ne, J4X, JCK, Kaysoft, Kral01, Madalad, Mike_Bello90, Noro, PASCAL, PENGUN, Proxy, Rickard, Shubham, SovaSlava, Strausses, Team_Rocket, ThreeSigma, Topmark, Udsen, Walter, Yanchuan, Zach_166, ZanyBonzy, adam-idarrha, adeolu, almurhasan, arjun16, ast3ros, asui, ayden, btk, cartlex_, castle_chain, cccz, chainsnake, codynhat, critical-or-high, cryptonue, csanuragjain, deepkin, degensec, dirk_y, erebus, foxb868, ge6a, hunter_w3b, jasonxiale, kkkmmmsk, lanrebayode77, lsaudit, marchev, matrix_0wl, max10afternoon, nuthan2x, oakcobalt, oxchsyston, pavankv, peanuts, pep7siup, pipidu83, pontifex, ptsanev, qpzm, radev_sw, rokinot, rotcivegaf, rvierdiiev, sorrynotsorry, squeaky_cactus, supersizer0x, tnquanghuy0512, twcctop, twicek, young, zhaojie, ziyou-
4.5226 USDC - $4.52
openzeppelin ERC20.sol
CONTRACT IMPORT CAN BE OMITTED IN THE USDe.sol
CONTRACTThe USDe.sol
contract imports the openzeppelin ERC20
contract to the USDe.sol
contract as shown below:
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
But the USDe
contract does not directly inherit from the ERC20.sol
contract but it is implicitly imported from the ERC20Burnable and ERC20Permit
contracts From which the USDe
contract inherits from.
Hence it is recommended to remove the redundant ERC20.sol
import.
https://github.com/code-423n4/2023-10-ethena/blob/main/contracts/USDe.sol#L4 https://github.com/code-423n4/2023-10-ethena/blob/main/contracts/USDe.sol#L15
_grantRole
DURING DEFAULT_ADMIN_ROLE
ASSIGNMENT IS NOT CHECKED FOR SUCCESS, IN THE EthenaMinting.constructor
FUNCTIONThe EthenaMinting.constructor
function sets the DEFAULT_ADMIN_ROLE
role to the deployer of the EthenaMinting
contract as shown below:
_grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
The _grantRole
is inherited from the openzeppelin AccessControl.sol
contract and the function returns a bool
value to indicate the success of the transaction. But this return value is not checked in the EthenaMinting.constructor
to ensure that the msg.sender
is succesfulyy assigned the DEFAULT_ADMIN_ROLE
.
Hence it is recommended to modify the above _grantRole
function call as shown below to check for the return
value.
bool success = _grantRole(DEFAULT_ADMIN_ROLE, msg.sender); require(success, "Role assignment failed");
https://github.com/code-423n4/2023-10-ethena/blob/main/contracts/EthenaMinting.sol#L124 https://github.com/code-423n4/2023-10-ethena/blob/main/contracts/EthenaMinting.sol#L139 https://github.com/code-423n4/2023-10-ethena/blob/main/contracts/StakedUSDe.sol#L79-L80
_revokeRole
FUNCTION CALL IS NOT CHECKED FOR SUCCESS IN THE EthenaMinting.removeMinterRole
AND EthenaMinting.removeRedeemerRole
FUNCTIONSThe EthenaMinting.removeMinterRole
function and the EthenaMinting.removeRedeemerRole
functions are used to revoke the MINTER_ROLE
and REDEEMER_ROLE
respectively from the addresses passed in by the GATEKEEPER_ROLE
.
The revoking of the roles are executed as shown below:
_revokeRole(MINTER_ROLE, minter); _revokeRole(REDEEMER_ROLE, redeemer);
The issue here is that the openzeppelin AccessControl._revokeRole
function returns a bool value which is not checked for success inside the removeMinterRole
function or removeRedeemerRole
function.
Hence even if the _revokeRole
function call is not executed succesfully the removeMinterRole
function and removeRedeemerRole
function will proceed exeuction thinking that the respective roles have been revoked for the passed in addresses.
Hence it is recommended to check the return boolean values of the removeMinterRole
and removeRedeemerRole
functions and revert the transaction if the return boolean value is false
.
https://github.com/code-423n4/2023-10-ethena/blob/main/contracts/EthenaMinting.sol#L277-L279 https://github.com/code-423n4/2023-10-ethena/blob/main/contracts/EthenaMinting.sol#L283-L285
order.beneficiary == address(0)
CONDITION IN THE EthenaMinting.verifyOrder
FUNCTIONThe EthenaMinting.verifyOrder
function is used to verify the order validity for a respective order. In this function there is a conditinal check to ensure that the order.beneficiary != address(0)
as shown below:
if (order.beneficiary == address(0)) revert InvalidAmount();
Here if the beneficiary
address is equal to the address(0)
the transaction will revert with the InvalidAmount()
error message. But this error message does not describe the actual root cause for the revert.
Hence it is recommendedt to create a new error as error InvalidBeneficiary()
and generate this error when the order.beneficiary == address(0)
condition occurs.
https://github.com/code-423n4/2023-10-ethena/blob/main/contracts/EthenaMinting.sol#L343
transfer
FUNCTION IS NOT CHECKED IN THE USDeSilo.withdraw
FUNCTIONThe USDeSilo.withdraw
function is used to withdraw the USDe
tokens once the cool down period is over. The USDeSilo.withdraw
function implementation is shown below:
function withdraw(address to, uint256 amount) external onlyStakingVault { USDE.transfer(to, amount); }
As it is shown above the return
value of the transfer
function is not checked for success
during the withdraw
function execution. And even though the openzeppeling SafeERC20 library
is imported into the USDe
the safeTransfer
is not used in the USDeSilo.withdraw
function.
As a result the USDeSilo.withdraw
transaction will be considered successful even if the claimed underlying assets are not correctly received by the receiving party.
Hence it is recommended to either check the return value of the transfer
function for success == true
condition or to use the safeTransfer
function in the USDeSilo.withdraw
function.
https://github.com/code-423n4/2023-10-ethena/blob/main/contracts/USDeSilo.sol#L28-L30
EthenaMinting.sol
DO NOT EMIT EVENTS AFTER THIER SUCCESFUL TRNASACTION EXECUTIONThe critical functions in the EthenaMinting.sol
contract such as the setMaxMintPerBlock
, setMaxRedeemPerBlock
and disableMintRedeem
do not emit events after the succesful execution of thier transactinos.
It is recommended to emit events in these critical functions after thier succesful execution for off-chain analysis.
https://github.com/code-423n4/2023-10-ethena/blob/main/contracts/EthenaMinting.sol#L219-L221 https://github.com/code-423n4/2023-10-ethena/blob/main/contracts/EthenaMinting.sol#L224-L226 https://github.com/code-423n4/2023-10-ethena/blob/main/contracts/EthenaMinting.sol#L229-L232
StakedUSDe.addToBlacklist
AND StakedUSDe.removeFromBlacklist
FUNCTIONSThe Natspec
comments of the StakedUSDe.addToBlacklist
and StakedUSDe.removeFromBlacklist
functions state that the owner of the StakedUSDe
contract can invoke the above functions as shown below:
* @notice Allows the owner (DEFAULT_ADMIN_ROLE) and blacklist managers to blacklist addresses.
But the actual function implementation has the onlyRole(BLACKLIST_MANAGER_ROLE)
which enables only the BLACKLIST_MANAGER_ROLE
to execute the above functions. Hence the DEFAULT_ADMIN_ROLE
can not execute the StakedUSDe.addToBlacklist
and StakedUSDe.removeFromBlacklist
functions unless the owner
is given the BLACKLIST_MANAGER_ROLE
too.
Hence it is recommended to update the Natspec
comments of the above two functions accordingly to indicate that the addresses with the BLACKLIST_MANAGER_ROLE
can only execute the StakedUSDe.addToBlacklist
and StakedUSDe.removeFromBlacklist
functions. The Natspec
comments should be modified as follows:
* @notice Allows the blacklist managers to blacklist addresses.
https://github.com/code-423n4/2023-10-ethena/blob/main/contracts/StakedUSDe.sol#L102 https://github.com/code-423n4/2023-10-ethena/blob/main/contracts/StakedUSDe.sol#L108 https://github.com/code-423n4/2023-10-ethena/blob/main/contracts/StakedUSDe.sol#L116 https://github.com/code-423n4/2023-10-ethena/blob/main/contracts/StakedUSDe.sol#L122
to
ADDRESS FOR address(0)
IN THE StakedUSDe.rescueTokens
FUNCTION AND HENCE RESCUED FUNDS COULD BE LOST IF SENT TO address(0)
The StakedUSDe.rescueTokens
function is used to rescue the tokens accidentally sent to the contract. This function is only callable by the DEFAULT_ADMIN_ROLE
. But there is no input validation on the to
address passed in. Hence if the address(0) is passed in as the to
address the rescued funds could be lost.
Hence it is recommended to perform the input validation on the to
address in the rescueTokens
function as shown below:
require(to != address(0), "to address can not be address(0)");
https://github.com/code-423n4/2023-10-ethena/blob/main/contracts/StakedUSDe.sol#L138-L141
#0 - c4-pre-sort
2023-11-02T01:29:09Z
raymondfam marked the issue as sufficient quality report
#1 - raymondfam
2023-11-02T01:31:26Z
5., 6., and 8. have been reported by the bot.
#2 - c4-judge
2023-11-14T17:11:13Z
fatherGoose1 marked the issue as grade-b
🌟 Selected for report: 0xVolcano
Also found by: 0x11singh99, 0xAadi, 0xAnah, 0xgrbr, 0xhacksmithh, 0xhex, 0xpiken, 0xta, J4X, JCK, K42, Raihan, Rolezn, SAQ, SM3_SS, Sathish9098, SovaSlava, ThreeSigma, Udsen, arjun16, aslanbek, brakelessak, castle_chain, evmboi32, hunter_w3b, lsaudit, naman1778, niser93, nuthan2x, oakcobalt, pavankv, petrichor, phenom80, radev_sw, shamsulhaq123, tabriz, thekmj, unique, yashgoel72, ybansal2403
6.4563 USDC - $6.46
getUnvestedAmount()
FUNCTION CALL CAN BE OMITTED WHILE CALCULATING THE newVestingAmount
VALUE IN THE StakedUSDe.transferInRewards
FUNCTION TO SAVE GASThe StakedUSDe.transferInRewards
function is used by the owner to transfer rewards from the controller contract into this contract.
Within the transferInRewards
function execution the newVestingAmount
variable is updated as follows:
if (getUnvestedAmount() > 0) revert StillVesting(); uint256 newVestingAmount = amount + getUnvestedAmount();
But as evident from the above code snippet the issue here is that the getUnvestedAmount()
calculation is redundant when calculating the newVestingAmount
since the transaction will revert if the getUnvestedAmount() > 0
.
Hence the only possiblity for transaction to pass the first if
statement is when the getUnvestedAmount() == 0
. Which means the amount + getUnvestedAmount()
is effectively equal to the amount + 0 = amount
.
Hence it is recommneded to omit the getUnvestedAmount()
function call while calculating the newVestingAmount
in the StakedUSDe.transferInRewards
function to save gas. The modified code to calculate the newVestingAmount
is shown below:
if (getUnvestedAmount() > 0) revert StillVesting(); uint256 newVestingAmount = amount;
https://github.com/code-423n4/2023-10-ethena/blob/main/contracts/StakedUSDe.sol#L90-L91
CALLDATA
VARIABLE CAN BE USED INPLACE OF STATE
VARIABLES WHEN EMITTING EVENTS TO SAVE GASThe EthenaMinting._setMaxMintPerBlock
function and the EthenaMinting._setMaxRedeemPerBlock
functions are used to set the maxMintPerBlock
and maxRedeemPerBlock
state variables respectively.
In each of the above functions once the state variables are set, events are emitted as shown below:
emit MaxMintPerBlockChanged(oldMaxMintPerBlock, maxMintPerBlock); emit MaxRedeemPerBlockChanged(oldMaxRedeemPerBlock, maxRedeemPerBlock);
Here the state
variables are used for the event emits where as the calldata
variables can be used in their place. This would save an extra SLOAD
operation. The modified lines of code are shown below:
emit MaxMintPerBlockChanged(oldMaxMintPerBlock, _maxMintPerBlock); emit MaxRedeemPerBlockChanged(oldMaxRedeemPerBlock, _maxRedeemPerBlock);
https://github.com/code-423n4/2023-10-ethena/blob/main/contracts/EthenaMinting.sol#L439 https://github.com/code-423n4/2023-10-ethena/blob/main/contracts/EthenaMinting.sol#L446 https://github.com/code-423n4/2023-10-ethena/blob/main/contracts/StakedUSDeV2.sol#L133
totalRatio
LOCAL VARIABLE TO 0
IN THE EthenaMinting.verifyRoute
FUNCTIONIn the EthenaMinting.verifyRoute
function the local variable totalRatio
is declared as follows:
uint256 totalRatio = 0;
It is recommended not to initialize the local variable to 0
since by default the value will be 0
for any local memory variable. Hence the totalRatio
variable declaration can be modified as follows to save gas.
uint256 totalRatio;
https://github.com/code-423n4/2023-10-ethena/blob/main/contracts/EthenaMinting.sol#L356 https://github.com/code-423n4/2023-10-ethena/blob/main/contracts/EthenaMinting.sol#L423
invalidator
VALUE RETRIEVAL BY CODE SNIPPET IN THE EthenaMinting.verifyNonce
FUNCTION TO SAVE GASThe EthenaMinting.verifyNonce
function is used to verify the order nonce during the execution of the EthenaMinting._deduplicateOrder
function.
During the verification of the nonce
the invalidator
is calculated as follows:
mapping(uint256 => uint256) storage invalidatorStorage = _orderBitmaps[sender]; uint256 invalidator = invalidatorStorage[invalidatorSlot];
But there is no need to declare the storage invalidatorStorage
mapping variable since the invalidator
can directly retrieved from the _orderBitmaps
as follows:
uint256 invalidator = _orderBitmaps[sender][invalidatorSlot];
Above modification to the invalidator
value retrieval will save gas on one SSTORE
and one SLOAD
opcode operation during the execution of the EthenaMinting.verifyNonce
function execution.
https://github.com/code-423n4/2023-10-ethena/blob/main/contracts/EthenaMinting.sol#L381-L382
delete
TO SET THE STORAGE VARIABLES TO DEFAULT VALUE OF 0
TO GAIN A GAS REFUNDThe StakedUSDeV2.unstake
function allows an user to claim the staking amount after the cooldown has finished. During the execution of the StakedUSDeV2.unstake
function the respective UserCooldown
struct parameters are set to 0
before the remaining underlying assets
are withdrawn to the user as shown below:
userCooldown.cooldownEnd = 0; userCooldown.underlyingAmount = 0;
But it is recommended to use the delete
to get a gas refunds instead of setting the parameters to 0
. Hence the above code should be modified as follows:
delete userCooldown.cooldownEnd; delete userCooldown.underlyingAmount;
https://github.com/code-423n4/2023-10-ethena/blob/main/contracts/StakedUSDeV2.sol#L83-L84
#0 - c4-pre-sort
2023-11-01T15:06:39Z
raymondfam marked the issue as sufficient quality report
#1 - c4-judge
2023-11-10T20:32:28Z
fatherGoose1 marked the issue as grade-b