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: 122/147
Findings: 1
Award: $4.52
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 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
EthenaMinting::mint()
function, the order duplication error will never occur.In mint()
function, Duplicate()
error is supposed to occur if _deduplicateOrder()
returns false.
function mint(Order calldata order, Route calldata route, Signature calldata signature) external override nonReentrant onlyRole(MINTER_ROLE) belowMaxMintPerBlock(order.usde_amount) { ... ... if (!_deduplicateOrder(order.benefactor, order.nonce)) revert Duplicate(); ... ... }
But in verifyNonce()
, if the given nonce duplicates, InvalidNonce()
error occurs instead of returning false, which means it won't reach the above revert.
So I would suggest modifying _deduplicateOrder()
and verifyNonce()
like below so that they can return nonce validation:
function verifyNonce(address sender, uint256 nonce) public view override returns (bool, uint256, uint256, uint256) { if (nonce == 0) revert InvalidNonce(); uint256 invalidatorSlot = uint64(nonce) >> 8; uint256 invalidatorBit = 1 << uint8(nonce); mapping(uint256 => uint256) storage invalidatorStorage = _orderBitmaps[sender]; uint256 invalidator = invalidatorStorage[invalidatorSlot]; bool invalidNonce = (invalidator & invalidatorBit != 0); return (!invalidNonce, invalidatorSlot, invalidator, invalidatorBit); } function _deduplicateOrder(address sender, uint256 nonce) private returns (bool) { (bool valid, uint256 invalidatorSlot, uint256 invalidator, uint256 invalidatorBit) = verifyNonce(sender, nonce); if (valid) { // Update invalidatorBit only if the nonce is valid mapping(uint256 => uint256) storage invalidatorStorage = _orderBitmaps[sender]; invalidatorStorage[invalidatorSlot] = invalidator | invalidatorBit; } return valid; }
nonce
is less than uint64.max in EthenaMinting::verifyNonce()
function.Verifying nonce is done under the assumption that the nonce is less than uint64.max
. If nonce is increased one by one from the user side, it is difficult to be more than uint64.max
, but if it is a random value, a duplication error may occur for different two nonces if one of them is more than uint64.max
.
So I suggest adding check if the nonce is less than the limit.
function verifyNonce(address sender, uint256 nonce) public view override returns (bool, uint256, uint256, uint256) { if (nonce == 0) revert InvalidNonce(); if (nonce >= uint64.max) revert InvalidNonce(); // --> Add here ... ... }
getUnvestedAmount()
can be removed in StakedUSDe::transferInRewards()
.In transferInRewards()
function, transferring rewards to the vault is prohibited when there is a remaining unvested amount.
if (getUnvestedAmount() > 0) revert StillVesting();
But in the next line, an unnecessary calculation is performed which is always 0.
uint256 newVestingAmount = amount + getUnvestedAmount();
So we can update the function like below:
function transferInRewards(uint256 amount) external nonReentrant onlyRole(REWARDER_ROLE) notZero(amount) { if (getUnvestedAmount() > 0) revert StillVesting(); vestingAmount = amount; // --> Here, update this line lastDistributionTimestamp = block.timestamp; // transfer assets from rewarder to this contract IERC20(asset()).safeTransferFrom(msg.sender, address(this), amount); emit RewardsReceived(amount, newVestingAmount); }
By standard AccessControl, a user can have multiple roles and this is same for FULL_RESTRICTED_STAKER_ROLE
and FULL_RESTRICTED_STAKER_ROLE
.
https://github.com/code-423n4/2023-10-ethena/blob/ee67d9b542642c9757a6b826c82d0cae60256509/contracts/StakedUSDe.sol#L106-L113 https://github.com/code-423n4/2023-10-ethena/blob/ee67d9b542642c9757a6b826c82d0cae60256509/contracts/StakedUSDe.sol#L120-L127
Even if a staker was released from one restriction role, it can be restricted by the other role.
So when adding or removing a restriction role, then either role should be considered in addToBlacklist()
and removeFromBlacklist()
.
vestingAmount
in StakingUSDe
I would highly suggest resetting vestingAmount
in the below:
function getUnvestedAmount() public view returns (uint256) { uint256 timeSinceLastDistribution = block.timestamp - lastDistributionTimestamp; if (timeSinceLastDistribution >= VESTING_PERIOD) { vestingAmount = 0; // -> Here, set vestingAmount to 0 return 0; } return ((VESTING_PERIOD - timeSinceLastDistribution) * vestingAmount) / VESTING_PERIOD; }
#0 - c4-pre-sort
2023-11-02T02:54:33Z
raymondfam marked the issue as sufficient quality report
#1 - c4-judge
2023-11-14T16:57:10Z
fatherGoose1 marked the issue as grade-b