The _checkMinShares() requirement called after any deposit (and withdrawal)
function _checkMinShares() internal view {
uint256 _totalSupply = totalSupply();
if (_totalSupply > 0 && _totalSupply < MIN_SHARES) revert MinSharesViolation();
}
reverts a first deposit which returns less than 1e18 shares. The share price is (totalSupply() + 1)/(totalAssets() + 1) so this first minimal deposit would normally be one USDe dollar, since 1e18 * (0 + 1)/(0 + 1) = 1e18. But by donating a USDe directly to the newly deployed StakedUSDeV2 the minimum first deposit becomes (a + 1) * 1e18, since (a + 1) * 1e18 * (0 + 1)/(a + 1) = 1e18.
A donation of as little as a penny (a = 1e16) thus forces the first deposit to be at least ten quadrillion and one USDe dollars ((1e16 + 1) * 1e18), which effectively bricks the contract.
Paste into StakedUSDe.t.sol
function testInflationBricking() public {
uint256 penny = 1e16;
uint256 gazillion = 1e34 + 1e18; // = (1e16 + 1) * MIN_SHARES = ten quadrillion and one dollars.
usdeToken.mint(alice, penny);
usdeToken.mint(bob, gazillion);
// Alice donates a penny to StakedUSDe
vm.startPrank(alice);
usdeToken.transfer(address(stakedUSDe), penny);
// Bob cannot deposit less than ten quadrillion and one dollars.
vm.startPrank(bob);
usdeToken.approve(address(stakedUSDe), gazillion);
vm.expectRevert(IStakedUSDe.MinSharesViolation.selector);
stakedUSDe.deposit(gazillion - 1, bob);
stakedUSDe.deposit(gazillion, bob);
assertEq(usdeToken.balanceOf(bob), 0);
assertEq(usdeToken.balanceOf(address(stakedUSDe)), gazillion + penny);
assertEq(stakedUSDe.balanceOf(bob), 1e18);
}
Recommended mitigation steps
Remove the minimum shares restriction and instead prevent inflation attacks by an initial deposit.
Lines of code
https://github.com/code-423n4/2023-10-ethena/blob/ee67d9b542642c9757a6b826c82d0cae60256509/contracts/StakedUSDe.sol#L214
Vulnerability details
Impact
StakedUSDeV2 can be bricked for a penny.
Proof of concept
The
_checkMinShares()
requirement called after any deposit (and withdrawal)reverts a first deposit which returns less than 1e18 shares. The share price is
(totalSupply() + 1)/(totalAssets() + 1)
so this first minimal deposit would normally be one USDe dollar, since1e18 * (0 + 1)/(0 + 1) = 1e18
. But by donatinga
USDe directly to the newly deployed StakedUSDeV2 the minimum first deposit becomes(a + 1) * 1e18
, since(a + 1) * 1e18 * (0 + 1)/(a + 1) = 1e18
. A donation of as little as a penny (a = 1e16
) thus forces the first deposit to be at least ten quadrillion and one USDe dollars ((1e16 + 1) * 1e18
), which effectively bricks the contract.Paste into StakedUSDe.t.sol
Recommended mitigation steps
Remove the minimum shares restriction and instead prevent inflation attacks by an initial deposit.
Assessed type
ERC4626