Skip to content
Open
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 @@ -75,6 +75,9 @@ contract OperatorStaking is ERC1363Upgradeable, ReentrancyGuardTransient, UUPSUp
/// @dev Thrown when the controller address is not valid (e.g., zero address).
error InvalidController();

/// @dev Thrown when the number of shares to redeem or request redeem is zero.
error InvalidShares();

modifier onlyOwner() {
require(msg.sender == owner(), CallerNotProtocolStakingOwner(msg.sender));
_;
Expand Down Expand Up @@ -170,9 +173,10 @@ contract OperatorStaking is ERC1363Upgradeable, ReentrancyGuardTransient, UUPSUp
* @param shares Amount of shares to redeem.
* @param controller The controller address for the request.
* @param ownerRedeem The owner of the shares.
* @return releaseTime The timestamp when the assets will be available for withdrawal.
*/
function requestRedeem(uint208 shares, address controller, address ownerRedeem) public virtual {
if (shares == 0) return;
function requestRedeem(uint208 shares, address controller, address ownerRedeem) public virtual returns (uint48) {
require(shares != 0, InvalidShares());
require(controller != address(0), InvalidController());
if (msg.sender != ownerRedeem) {
_spendAllowance(ownerRedeem, msg.sender, shares);
Expand All @@ -196,6 +200,8 @@ contract OperatorStaking is ERC1363Upgradeable, ReentrancyGuardTransient, UUPSUp
$._redeemRequests[controller].push(releaseTime, controllerSharesRedeemed + shares);

emit RedeemRequest(controller, ownerRedeem, 0, msg.sender, shares, releaseTime);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just realized this now, but we no longer need the 3rd argument here, which is always 0, in the RedeemRequest event, initially it was only here because we tried to adhere to the ERC7540 (async vaults), but since we dont follow this ERC anyways, especially now that we added the releaseTime in the event, I think we could remove it... wdyt?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But if we do this, we will probably need to update the front-end app as well, so it might be too late actually.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we will update this in #1681 + consider the change in the frontend once it is merged yes !


return releaseTime;
}

/**
Expand All @@ -210,6 +216,7 @@ contract OperatorStaking is ERC1363Upgradeable, ReentrancyGuardTransient, UUPSUp
address receiver,
address controller
) public virtual nonReentrant returns (uint256) {
require(shares != 0, InvalidShares());
require(msg.sender == controller || isOperator(controller, msg.sender), Unauthorized());

uint256 maxShares = maxRedeem(controller);
Expand Down
42 changes: 34 additions & 8 deletions protocol-contracts/staking/test/OperatorStaking.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,9 +210,21 @@ describe('OperatorStaking', function () {
describe('redeem', async function () {
it('simple redemption', async function () {
await this.mock.connect(this.delegator1).deposit(ethers.parseEther('1'), this.delegator1);
await this.mock
.connect(this.delegator1)
.requestRedeem(await this.mock.balanceOf(this.delegator1), this.delegator1, this.delegator1);
const currentTimestamp = await time.latest();
await expect(
this.mock
.connect(this.delegator1)
.requestRedeem(await this.mock.balanceOf(this.delegator1), this.delegator1, this.delegator1),
)
.to.emit(this.mock, 'RedeemRequest')
.withArgs(
this.delegator1,
this.delegator1,
0,
this.delegator1,
ethers.parseEther('1'),
BigInt(currentTimestamp) + 1n + (await this.protocolStaking.unstakeCooldownPeriod()),
);

await expect(this.mock.pendingRedeemRequest(0, this.delegator1)).to.eventually.eq(ethers.parseEther('1'));
await expect(this.mock.claimableRedeemRequest(0, this.delegator1)).to.eventually.eq(0);
Expand All @@ -228,11 +240,13 @@ describe('OperatorStaking', function () {
await expect(this.token.balanceOf(this.mock)).to.eventually.be.eq(0);
});

it('zero redemption should terminate early', async function () {
await expect(this.mock.connect(this.delegator1).requestRedeem(0, this.delegator1, this.delegator1)).to.not.emit(
this.mock,
'RedeemRequest',
);
it('should return release time', async function () {
await this.mock.connect(this.delegator1).deposit(ethers.parseEther('1'), this.delegator1);
await expect(
this.mock
.connect(this.delegator1)
.requestRedeem.staticCall(await this.mock.balanceOf(this.delegator1), this.delegator1, this.delegator1),
).to.eventually.eq(BigInt(await time.latest()) + (await this.protocolStaking.unstakeCooldownPeriod()));
});

it('should not redeem twice', async function () {
Expand All @@ -251,6 +265,18 @@ describe('OperatorStaking', function () {
).to.not.emit(this.token, 'Transfer');
});

it('should revert on requestRedeem 0 shares', async function () {
await expect(
this.mock.connect(this.delegator1).requestRedeem(0, this.delegator1, this.delegator1),
).to.be.revertedWithCustomError(this.mock, 'InvalidShares');
});

it('should revert on redeem 0 shares', async function () {
await expect(
this.mock.connect(this.delegator1).redeem(0, this.delegator1, this.delegator1),
).to.be.revertedWithCustomError(this.mock, 'InvalidShares');
});

it('should revert on redeem more than available', async function () {
await this.mock.connect(this.delegator1).deposit(ethers.parseEther('10'), this.delegator1);
await this.mock.connect(this.delegator1).requestRedeem(ethers.parseEther('1'), this.delegator1, this.delegator1);
Expand Down