Closed 0xKitsune closed 1 year ago
Note that this would not be an issue if the operator uses a mev-relay as standard.
Another mitigation technique is not allow a user to deposit and withdraw in the same block.
Personally I prefer using exact accounting as we do instead of smoothing with unrealized profits / losses, as this too would have attack vectors and generally favour shorter term holders over longer term.
Example of denying a user of depositing and withdrawing in the same block:
mapping(address => uint256) lastTransactionBlock;
function deposit(uint256 amount) public {
require(lastTransactionBlock[msg.sender] != block.number, "You can't deposit and withdraw in the same block");
// Your deposit logic here
// Update last transaction block for the user
lastTransactionBlock[msg.sender] = block.number;
}
function withdraw(uint256 amount) public {
require(lastTransactionBlock[msg.sender] != block.number, "You can't deposit and withdraw in the same block");
// Your withdrawal logic here
// Update last transaction block for the user
lastTransactionBlock[msg.sender] = block.number;
}
Another mitigation technique (slightly better than above) is to deny withdrawals within a certain time period.
uint64 public LOCKUP = 1 days;
mapping(address => uint256) public depositTimestamps;
function _updateDepositTimestamp(address account, uint256 shares) internal {
// Set the deposit timestamp for the user
uint256 prevBalance = balanceOf[account];
if (_isZero(prevBalance)) {
depositTimestamps[account] = block.timestamp;
} else {
// multiple deposits, so weight timestamp by amounts
unchecked {
depositTimestamps[account] = ((depositTimestamps[account] * prevBalance) + (block.timestamp * shares)) / (prevBalance + shares);
}
}
}
function deposit(uint256 amount) public {
// Your deposit logic here
_updateDepositTimestamp(msg.sender, amount);
}
function withdraw(uint256 amount) public {
if (block.timestamp < depositTimestamps[msg.sender] + LOCKUP) revert InsufficientLockupTime();
// Your withdrawal logic here
}
In both cases, transfer
and transferFrom
would need to be overriden to update these mappings.
A better mitigation than the 2 above is to implement a withdraw fee
@aldoborrero @ControlCplusControlV @0xKitsune Thoughts?
I'm not sure, adding a fee on Withdraw seems like it could cause a lot of issues and potentially lead to a lot of weird ERC4626 bugs
Withdraw fees are normal for ERC4626. The specs mention specifically where withdraw fees should be included. https://eips.ethereum.org/EIPS/eip-4626
Implementing the fee on withdraw does not effect passing all external ERC4626 tests: https://github.com/manifoldfinance/mevETH2/pull/164
Status
Reported
Type
Vulnerability
Severity
Highest
Code Snippet:
Remediation
The root of the issue is how rewards distribution is currently designed and also a common challenge with liquid staking protocols.
The share rate should be based on the existing funds as well as any unrealised profit from validator nodes. This is similar to how lending protocols should take any outstanding debt and accumulated interest into account even if the loan has not yet been repaid.
The unrealised profit could be calculated from an APR based on all previous rewards or on a constant rate multiplied by the number of validators (and assuming they do not get penalised on the consensus layer).
Description