Skip to content
Closed
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
41 changes: 41 additions & 0 deletions contracts/contracts/yield/YieldManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -619,6 +619,47 @@ contract YieldManager is
whenTypeAndGeneralNotPaused(PauseType.NATIVE_YIELD_UNSTAKING)
onlyKnownYieldProvider(_yieldProvider)
onlyRole(YIELD_PROVIDER_UNSTAKER_ROLE)
{
_withdrawFromYieldProvider(_yieldProvider, _amount);
}

/**
* @notice Safely withdraws ETH from a YieldProvider, capped by the available withdrawable amount.
* @dev YIELD_PROVIDER_UNSTAKER_ROLE is required to execute.
* @dev This function behaves like {withdrawFromYieldProvider}, but ensures the requested `_amount`
* does not exceed the provider’s currently withdrawable value, preventing reverts due to
* over-withdrawal.
* @param _yieldProvider The yield provider address.
* @param _amount The desired amount to withdraw (subject to capping by the withdrawable value).
*/
function safeWithdrawFromYieldProvider(
address _yieldProvider,
uint256 _amount
)
external
whenTypeAndGeneralNotPaused(PauseType.NATIVE_YIELD_UNSTAKING)
onlyKnownYieldProvider(_yieldProvider)
onlyRole(YIELD_PROVIDER_UNSTAKER_ROLE)
{
_withdrawFromYieldProvider(_yieldProvider, Math256.min(withdrawableValue(_yieldProvider), _amount));
}

/**
* @notice Withdraw ETH from a YieldProvider.
* @dev YIELD_PROVIDER_UNSTAKER_ROLE is required to execute.
* @dev This function proactively allocates withdrawn funds in the following priority:
* 1. If the withdrawal reserve is below the target threshold, ETH is routed to the reserve
* to restore the deficit.
* 2. If there is an outstanding LST liability, it will be paid.
* 3. YieldManager will keep the remainder.
* @param _yieldProvider The yield provider address.
* @param _amount Amount to withdraw.
*/
function _withdrawFromYieldProvider(
address _yieldProvider,
uint256 _amount
)
internal
{
uint256 targetDeficit = getTargetReserveDeficit();
// Withdraw from Vault -> YieldManager
Expand Down
89 changes: 89 additions & 0 deletions contracts/test/yield/unit/YieldManager.funds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1002,6 +1002,95 @@ describe("YieldManager contract - ETH transfer operations", () => {
});
});

describe("safe withdraw from yield provider", () => {
it("Should revert when the GENERAL pause type is activated", async () => {
const { mockYieldProviderAddress } = await addMockYieldProvider(yieldManager);

await yieldManager.connect(securityCouncil).pauseByType(GENERAL_PAUSE_TYPE);

await expectRevertWithCustomError(
yieldManager,
yieldManager.connect(nativeYieldOperator).safeWithdrawFromYieldProvider(mockYieldProviderAddress, 1n),
"IsPaused",
[GENERAL_PAUSE_TYPE],
);
});

it("Should revert when the NATIVE_YIELD_UNSTAKING pause type is activated", async () => {
const { mockYieldProviderAddress } = await addMockYieldProvider(yieldManager);

await yieldManager.connect(securityCouncil).pauseByType(NATIVE_YIELD_UNSTAKING_PAUSE_TYPE);

await expectRevertWithCustomError(
yieldManager,
yieldManager.connect(nativeYieldOperator).safeWithdrawFromYieldProvider(mockYieldProviderAddress, 1n),
"IsPaused",
[NATIVE_YIELD_UNSTAKING_PAUSE_TYPE],
);
});

it("Should revert when unstaking from an unknown YieldProvider", async () => {
const unknownYieldProvider = ethers.Wallet.createRandom().address;

await expectRevertWithCustomError(
yieldManager,
yieldManager.connect(nativeYieldOperator).safeWithdrawFromYieldProvider(unknownYieldProvider, 1n),
"UnknownYieldProvider",
);
});

it("Should revert when the caller does not have YIELD_PROVIDER_UNSTAKER_ROLE role", async () => {
const { mockYieldProviderAddress } = await addMockYieldProvider(yieldManager);
const unstakerRole = await yieldManager.YIELD_PROVIDER_UNSTAKER_ROLE();

await expect(
yieldManager.connect(nonAuthorizedAccount).safeWithdrawFromYieldProvider(mockYieldProviderAddress, 1n),
).to.be.revertedWith(buildAccessErrorMessage(nonAuthorizedAccount, unstakerRole));
});

it("If _amount < withdrawableValue, should withdraw _amount", async () => {
const { mockYieldProviderAddress, mockYieldProvider } = await addMockYieldProvider(yieldManager);
const withdrawableAmount = ONE_ETHER * 2n;
const requestedAmount = ONE_ETHER;

await fundYieldProviderForWithdrawal(yieldManager, mockYieldProvider, nativeYieldOperator, withdrawableAmount);
await setWithdrawalReserveToTarget(yieldManager);
expect(await yieldManager.getTargetReserveDeficit()).to.equal(0n);

await expect(
yieldManager
.connect(nativeYieldOperator)
.safeWithdrawFromYieldProvider(mockYieldProviderAddress, requestedAmount),
)
.to.emit(yieldManager, "YieldProviderWithdrawal")
.withArgs(mockYieldProviderAddress, requestedAmount, requestedAmount, 0n, 0n);

expect(await yieldManager.userFunds(mockYieldProviderAddress)).to.equal(withdrawableAmount - requestedAmount);
expect(await yieldManager.userFundsInYieldProvidersTotal()).to.equal(withdrawableAmount - requestedAmount);
});

it("If withdrawableValue < _amount , should withdraw withdrawableValue", async () => {
const { mockYieldProviderAddress, mockYieldProvider } = await addMockYieldProvider(yieldManager);
const withdrawableAmount = ONE_ETHER;
const requestedAmount = withdrawableAmount * 2n;

await fundYieldProviderForWithdrawal(yieldManager, mockYieldProvider, nativeYieldOperator, withdrawableAmount);
await setWithdrawalReserveToTarget(yieldManager);
expect(await yieldManager.getTargetReserveDeficit()).to.equal(0n);

await expect(
yieldManager
.connect(nativeYieldOperator)
.safeWithdrawFromYieldProvider(mockYieldProviderAddress, requestedAmount),
)
.to.emit(yieldManager, "YieldProviderWithdrawal")
.withArgs(mockYieldProviderAddress, withdrawableAmount, withdrawableAmount, 0n, 0n);

expect(await yieldManager.userFunds(mockYieldProviderAddress)).to.equal(0n);
expect(await yieldManager.userFundsInYieldProvidersTotal()).to.equal(0n);
});
});

describe("adding to withdrawal reserve", () => {
it("Should revert when the GENERAL pause type is activated", async () => {
const { mockYieldProviderAddress } = await addMockYieldProvider(yieldManager);
Expand Down
Loading