diff --git a/contracts/interfaces/IStakeManager.sol b/contracts/interfaces/IStakeManager.sol new file mode 100644 index 0000000..c19c1ba --- /dev/null +++ b/contracts/interfaces/IStakeManager.sol @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity ^0.8.12; + +/** + * manage deposits and stakes. + * deposit is just a balance used to pay for UserOperations (either by a paymaster or an account) + * stake is value locked for at least "unstakeDelay" by the staked entity. + */ +interface IStakeManager { + + event Deposited( + address indexed account, + uint256 totalDeposit + ); + + event Withdrawn( + address indexed account, + address withdrawAddress, + uint256 amount + ); + + /// Emitted when stake or unstake delay are modified + event StakeLocked( + address indexed account, + uint256 totalStaked, + uint256 unstakeDelaySec + ); + + /// Emitted once a stake is scheduled for withdrawal + event StakeUnlocked( + address indexed account, + uint256 withdrawTime + ); + + event StakeWithdrawn( + address indexed account, + address withdrawAddress, + uint256 amount + ); + + /** + * @param deposit the entity's deposit + * @param staked true if this entity is staked. + * @param stake actual amount of ether staked for this entity. + * @param unstakeDelaySec minimum delay to withdraw the stake. + * @param withdrawTime - first block timestamp where 'withdrawStake' will be callable, or zero if already locked + * @dev sizes were chosen so that (deposit,staked, stake) fit into one cell (used during handleOps) + * and the rest fit into a 2nd cell. + * 112 bit allows for 10^15 eth + * 48 bit for full timestamp + * 32 bit allows 150 years for unstake delay + */ + struct DepositInfo { + uint112 deposit; + bool staked; + uint112 stake; + uint32 unstakeDelaySec; + uint48 withdrawTime; + } + + //API struct used by getStakeInfo and simulateValidation + struct StakeInfo { + uint256 stake; + uint256 unstakeDelaySec; + } + + /// @return info - full deposit information of given account + function getDepositInfo(address account) external view returns (DepositInfo memory info); + + /// @return the deposit (for gas payment) of the account + function balanceOf(address account) external view returns (uint256); + + /** + * add to the deposit of the given account + */ + function depositTo(address account) external payable; + + /** + * add to the account's stake - amount and delay + * any pending unstake is first cancelled. + * @param _unstakeDelaySec the new lock duration before the deposit can be withdrawn. + */ + function addStake(uint32 _unstakeDelaySec) external payable; + + /** + * attempt to unlock the stake. + * the value can be withdrawn (using withdrawStake) after the unstake delay. + */ + function unlockStake() external; + + /** + * withdraw from the (unlocked) stake. + * must first call unlockStake and wait for the unstakeDelay to pass + * @param withdrawAddress the address to send withdrawn value. + */ + function withdrawStake(address payable withdrawAddress) external; + + /** + * withdraw from the deposit. + * @param withdrawAddress the address to send withdrawn value. + * @param withdrawAmount the amount to withdraw. + */ + function withdrawTo(address payable withdrawAddress, uint256 withdrawAmount) external; +} diff --git a/contracts/predeploys/StakeManager.sol b/contracts/predeploys/StakeManager.sol new file mode 100644 index 0000000..90c4935 --- /dev/null +++ b/contracts/predeploys/StakeManager.sol @@ -0,0 +1,124 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity ^0.8.12; + +import "../interfaces/IStakeManager.sol"; + +/* solhint-disable avoid-low-level-calls */ +/* solhint-disable not-rely-on-time */ +/** + * manage deposits and stakes. + * deposit is just a balance used to pay for UserOperations (either by a paymaster or an account) + * stake is value locked for at least "unstakeDelay" by a paymaster. + */ +contract StakeManager is IStakeManager { + + /// maps paymaster to their deposits and stakes + mapping(address => DepositInfo) public deposits; + + /// @inheritdoc IStakeManager + function getDepositInfo(address account) public view returns (DepositInfo memory info) { + return deposits[account]; + } + + // internal method to return just the stake info + function _getStakeInfo(address addr) internal view returns (StakeInfo memory info) { + DepositInfo storage depositInfo = deposits[addr]; + info.stake = depositInfo.stake; + info.unstakeDelaySec = depositInfo.unstakeDelaySec; + } + + /// return the deposit (for gas payment) of the account + function balanceOf(address account) public view returns (uint256) { + return deposits[account].deposit; + } + + receive() external payable { + depositTo(msg.sender); + } + + function _incrementDeposit(address account, uint256 amount) internal { + DepositInfo storage info = deposits[account]; + uint256 newAmount = info.deposit + amount; + require(newAmount <= type(uint112).max, "deposit overflow"); + info.deposit = uint112(newAmount); + } + + /** + * add to the deposit of the given account + */ + function depositTo(address account) public payable { + _incrementDeposit(account, msg.value); + DepositInfo storage info = deposits[account]; + emit Deposited(account, info.deposit); + } + + /** + * add to the account's stake - amount and delay + * any pending unstake is first cancelled. + * @param unstakeDelaySec the new lock duration before the deposit can be withdrawn. + */ + function addStake(uint32 unstakeDelaySec) public payable { + DepositInfo storage info = deposits[msg.sender]; + require(unstakeDelaySec > 0, "must specify unstake delay"); + require(unstakeDelaySec >= info.unstakeDelaySec, "cannot decrease unstake time"); + uint256 stake = info.stake + msg.value; + require(stake > 0, "no stake specified"); + require(stake <= type(uint112).max, "stake overflow"); + deposits[msg.sender] = DepositInfo( + info.deposit, + true, + uint112(stake), + unstakeDelaySec, + 0 + ); + emit StakeLocked(msg.sender, stake, unstakeDelaySec); + } + + /** + * attempt to unlock the stake. + * the value can be withdrawn (using withdrawStake) after the unstake delay. + */ + function unlockStake() external { + DepositInfo storage info = deposits[msg.sender]; + require(info.unstakeDelaySec != 0, "not staked"); + require(info.staked, "already unstaking"); + uint48 withdrawTime = uint48(block.timestamp) + info.unstakeDelaySec; + info.withdrawTime = withdrawTime; + info.staked = false; + emit StakeUnlocked(msg.sender, withdrawTime); + } + + + /** + * withdraw from the (unlocked) stake. + * must first call unlockStake and wait for the unstakeDelay to pass + * @param withdrawAddress the address to send withdrawn value. + */ + function withdrawStake(address payable withdrawAddress) external { + DepositInfo storage info = deposits[msg.sender]; + uint256 stake = info.stake; + require(stake > 0, "No stake to withdraw"); + require(info.withdrawTime > 0, "must call unlockStake() first"); + require(info.withdrawTime <= block.timestamp, "Stake withdrawal is not due"); + info.unstakeDelaySec = 0; + info.withdrawTime = 0; + info.stake = 0; + emit StakeWithdrawn(msg.sender, withdrawAddress, stake); + (bool success,) = withdrawAddress.call{value : stake}(""); + require(success, "failed to withdraw stake"); + } + + /** + * withdraw from the deposit. + * @param withdrawAddress the address to send withdrawn value. + * @param withdrawAmount the amount to withdraw. + */ + function withdrawTo(address payable withdrawAddress, uint256 withdrawAmount) external { + DepositInfo storage info = deposits[msg.sender]; + require(withdrawAmount <= info.deposit, "Withdraw amount too large"); + info.deposit = uint112(info.deposit - withdrawAmount); + emit Withdrawn(msg.sender, withdrawAddress, withdrawAmount); + (bool success,) = withdrawAddress.call{value : withdrawAmount}(""); + require(success, "failed to withdraw"); + } +}