Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions protocol-contracts/staking/contracts/OperatorStaking.sol
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,9 @@ contract OperatorStaking is ERC1363Upgradeable, ReentrancyGuardTransient, UUPSUp
/// @dev Thrown when the number of shares to redeem or request redeem is zero.
error InvalidShares();

/// @dev Thrown when liquid asset balance is insufficient to cover pending redemptions in {stakeExcess}.
error NoExcessBalance(uint256 liquidBalance, uint256 assetsPendingRedemption);

modifier onlyOwner() {
require(msg.sender == owner(), CallerNotProtocolStakingOwner(msg.sender));
_;
Expand Down Expand Up @@ -262,12 +265,16 @@ contract OperatorStaking is ERC1363Upgradeable, ReentrancyGuardTransient, UUPSUp
*
* NOTE: Excess tokens will be in the `OperatorStaking` contract the operator is slashed
* during a redemption flow or if donations are made to it. Anyone can call this function to
* restake those tokens.
* restake those tokens. This function will revert if the liquid balance is less than or equal
* to the amount of assets in pending redemption .
*/
function stakeExcess() public virtual {
ProtocolStaking protocolStaking_ = protocolStaking();
protocolStaking_.release(address(this));
uint256 amountToRestake = IERC20(asset()).balanceOf(address(this)) - previewRedeem(totalSharesInRedemption());
uint256 liquidBalance = IERC20(asset()).balanceOf(address(this));
uint256 assetsPendingRedemption = previewRedeem(totalSharesInRedemption());
require(liquidBalance > assetsPendingRedemption, NoExcessBalance(liquidBalance, assetsPendingRedemption));
uint256 amountToRestake = liquidBalance - assetsPendingRedemption;
protocolStaking_.stake(amountToRestake);
}

Expand Down
15 changes: 15 additions & 0 deletions protocol-contracts/staking/test/OperatorStaking.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -487,6 +487,21 @@ describe('OperatorStaking', function () {
.to.emit(this.token, 'Transfer')
.withArgs(this.mock, this.protocolStaking, restakeAmount);
});

it('should revert with NoExcessBalance when liquid balance is less than pending redemptions', async function () {
// Deposit and request redemption
await this.mock.connect(this.delegator1).deposit(ethers.parseEther('10'), this.delegator1);
await this.mock.connect(this.delegator1).requestRedeem(ethers.parseEther('5'), this.delegator1, this.delegator1);

// Slash staked balance to reduce available funds (slash 3 assets)
await this.protocolStaking.slashWithdrawal(this.mock, ethers.parseEther('3'));

await timeIncreaseNoMine(60);

// Now liquid balance (after release) will be less than assets pending redemption
// since slashing reduced the staked balance but pending redemption still expects original value
await expect(this.mock.stakeExcess()).to.be.revertedWithCustomError(this.mock, 'NoExcessBalance');
});
});

describe('slashing', async function () {
Expand Down