Lines of code
<https://github.com/code-423n4/2023-10-ethena/blob/ee67d9b542642c9757a6b826c82d0cae60256509/contracts/StakedUSDe.sol#L138-L141>
The StakedUSDe contract can be accidentally blocked if the all shares will be redeemed before the VESTING_PERIOD end. The _checkMinShares function will then revert for any eligible deposits. The same result will be in case of asset transferring to the contract while the totalSupply() equals zero.
There is the _checkMinShares check at the _deposit and _withdraw functions which should prevent a donation attack.
function _deposit(address caller, address receiver, uint256 assets, uint256 shares)
internal
override
nonReentrant
notZero(assets)
notZero(shares)
{
if (hasRole(SOFT_RESTRICTED_STAKER_ROLE, caller) || hasRole(SOFT_RESTRICTED_STAKER_ROLE, receiver)) {
revert OperationNotAllowed();
}
super._deposit(caller, receiver, assets, shares);
_checkMinShares();
}
function _withdraw(address caller, address receiver, address _owner, uint256 assets, uint256 shares)
internal
override
nonReentrant
notZero(assets)
notZero(shares)
{
if (hasRole(FULL_RESTRICTED_STAKER_ROLE, caller) || hasRole(FULL_RESTRICTED_STAKER_ROLE, receiver)) {
revert OperationNotAllowed();
}
super._withdraw(caller, receiver, _owner, assets, shares);
_checkMinShares();
}
This check can not prevent case then accidental amount of the assets on the contract which was left after all shares were redeemed. It can easily happen if shares were redeemed before the VESTING_PERIOD end.
function totalAssets() public view override returns (uint256) {
return IERC20(asset()).balanceOf(address(this)) - getUnvestedAmount();
}
function getUnvestedAmount() public view returns (uint256) {
uint256 timeSinceLastDistribution = block.timestamp - lastDistributionTimestamp;
if (timeSinceLastDistribution >= VESTING_PERIOD) {
return 0;
}
return ((VESTING_PERIOD - timeSinceLastDistribution) * vestingAmount) / VESTING_PERIOD;
}
If some assets left and at the same time totalSupply is zero the rate between assets and shares become unbalanced and deposit will revert for normal amounts of assets.
Letβs look at the ERC4626 contract:
function _convertToShares(uint256 assets, Math.Rounding rounding) internal view virtual returns (uint256) {
return assets.mulDiv(totalSupply() + 10 ** _decimalsOffset(), totalAssets() + 1, rounding);
}
For example there is 1.0 * 1018 of assets on the contract balance, totalSupply is zero and an user want to deposit $100k:
100 000 * 1018 * 100 / (1.0 * 1018 + 1) < MIN_SHARES == 10**18
So the _checkMinShares will revert:
function _checkMinShares() internal view {
uint256 _totalSupply = totalSupply();
if (_totalSupply > 0 && _totalSupply < MIN_SHARES) revert MinSharesViolation();
}
These assets can not be withdrawn even by the DEFAULT_ADMIN_ROLE because of check in the rescueTokens. So the contract will have to be deployed again:
function rescueTokens(address token, uint256 amount, address to) external onlyRole(DEFAULT_ADMIN_ROLE) {
if (address(token) == asset()) revert InvalidToken();
IERC20(token).safeTransfer(to, amount);
}
Manual review
It is possible to let DEFAULT_ADMIN_ROLE withdraw assets in case totalSupply equals zero to prevent permanent DoS. Also possible to deposit $1 and as receiver put this contract. Then the _checkMinShares check will be redundant.
DoS
The text was updated successfully, but these errors were encountered:
All reactions