Skip to content

Commit

Permalink
fix: remove stETH withdraws from Kelp in favor of ETH withdraws
Browse files Browse the repository at this point in the history
  • Loading branch information
jeffywu committed Aug 12, 2024
1 parent 9c9f7da commit de862c8
Show file tree
Hide file tree
Showing 6 changed files with 49 additions and 187 deletions.
15 changes: 4 additions & 11 deletions contracts/vaults/staking/PendlePTKelpVault.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {Constants} from "@contracts/global/Constants.sol";
import {TypeConvert} from "@contracts/global/TypeConvert.sol";
import {Deployments} from "@deployments/Deployments.sol";
import {PendlePrincipalToken, WithdrawRequest} from "./protocols/PendlePrincipalToken.sol";
import { KelpLib, KelpCooldownHolder, rsETH, stETH } from "./protocols/Kelp.sol";
import { KelpLib, KelpCooldownHolder, rsETH} from "./protocols/Kelp.sol";

contract PendlePTKelpVault is PendlePrincipalToken {
using TypeConvert for int256;
Expand Down Expand Up @@ -44,9 +44,9 @@ contract PendlePTKelpVault is PendlePrincipalToken {
) internal override view returns (uint256) {
uint256 tokenOutSY = getTokenOutSYForWithdrawRequest(requestId);
// NOTE: in this vault the tokenOutSy is known to be stETH.
(int256 stETHPrice, /* */) = TRADING_MODULE.getOraclePrice(TOKEN_OUT_SY, BORROW_TOKEN);
return (tokenOutSY * stETHPrice.toUint() * BORROW_PRECISION) /
(KelpLib.stETH_PRECISION * Constants.EXCHANGE_RATE_PRECISION);
(int256 ethPrice, /* */) = TRADING_MODULE.getOraclePrice(TOKEN_OUT_SY, BORROW_TOKEN);
return (tokenOutSY * ethPrice.toUint() * BORROW_PRECISION) /
(Constants.EXCHANGE_RATE_PRECISION * Constants.EXCHANGE_RATE_PRECISION);
}

function _initiateSYWithdraw(
Expand All @@ -65,11 +65,4 @@ contract PendlePTKelpVault is PendlePrincipalToken {
return KelpLib._canFinalizeWithdrawRequest(requestId);
}

function canTriggerExtraStep(uint256 requestId) public view returns (bool) {
return KelpLib._canTriggerExtraStep(requestId);
}

function triggerExtraStep(uint256 requestId) external {
KelpLib._triggerExtraStep(requestId);
}
}
80 changes: 9 additions & 71 deletions contracts/vaults/staking/protocols/Kelp.sol
Original file line number Diff line number Diff line change
Expand Up @@ -26,28 +26,8 @@ interface IWithdrawalManager {
) external returns (uint256 rsETHBurned, uint256 assetAmountUnlocked);
}

interface ILidoWithdraw {
struct WithdrawalRequestStatus {
uint256 amountOfStETH;
uint256 amountOfShares;
address owner;
uint256 timestamp;
bool isFinalized;
bool isClaimed;
}

function requestWithdrawals(uint256[] memory _amounts, address _owner) external returns (uint256[] memory requestIds);
function getWithdrawalRequests(address _owner) external view returns (uint256[] memory requestsIds);
function getWithdrawalStatus(uint256[] memory _requestIds) external view returns (WithdrawalRequestStatus[] memory statuses);
function claimWithdrawal(uint256 _requestId) external;
function finalize(uint256 _lastRequestIdToBeFinalized, uint256 _maxShareRate) external payable;
function getLastRequestId() external view returns (uint256);
}

IERC20 constant rsETH = IERC20(0xA1290d69c65A6Fe4DF752f95823fae25cB99e5A7);
IWithdrawalManager constant WithdrawManager = IWithdrawalManager(0x62De59c08eB5dAE4b7E6F7a8cAd3006d6965ec16);
address constant stETH = 0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84;
ILidoWithdraw constant LidoWithdraw = ILidoWithdraw(0x889edC2eDab5f40e902b864aD4d7AdE8E412F9B1);

contract KelpCooldownHolder is ClonedCoolDownHolder {
bool public triggered = false;
Expand All @@ -63,41 +43,18 @@ contract KelpCooldownHolder is ClonedCoolDownHolder {
uint256 balance = rsETH.balanceOf(address(this));
rsETH.approve(address(WithdrawManager), balance);
// initiate withdraw from Kelp
WithdrawManager.initiateWithdrawal(stETH, balance);
}

/// @notice this method need to be called once withdraw on Kelp is finalized
/// to start withdraw process from Lido so we can unwrap stETH to ETH
/// since we are not able to withdraw ETH directly from Kelp
function triggerExtraStep() external {
require(!triggered);
(/* */, /* */, /* */, uint256 userWithdrawalRequestNonce) = WithdrawManager.getUserWithdrawalRequest(stETH, address(this), 0);
require(userWithdrawalRequestNonce < WithdrawManager.nextLockedNonce(stETH));

WithdrawManager.completeWithdrawal(stETH);
uint256 tokensClaimed = IERC20(stETH).balanceOf(address(this));

uint256[] memory amounts = new uint256[](1);
amounts[0] = tokensClaimed;
IERC20(stETH).approve(address(LidoWithdraw), amounts[0]);
LidoWithdraw.requestWithdrawals(amounts, address(this));

triggered = true;
WithdrawManager.initiateWithdrawal(Deployments.ALT_ETH_ADDRESS, balance);
}

function _finalizeCooldown() internal override returns (uint256 tokensClaimed, bool finalized) {
if (!triggered) {
return (0, false);
}

uint256[] memory requestIds = LidoWithdraw.getWithdrawalRequests(address(this));
ILidoWithdraw.WithdrawalRequestStatus[] memory withdrawsStatus = LidoWithdraw.getWithdrawalStatus(requestIds);

if (!withdrawsStatus[0].isFinalized) {
(/* */, /* */, /* */, uint256 userWithdrawalRequestNonce) = WithdrawManager.getUserWithdrawalRequest(Deployments.ALT_ETH_ADDRESS, address(this), 0);
uint256 nextNonce = WithdrawManager.nextLockedNonce(Deployments.ALT_ETH_ADDRESS);
// Will revert when nextLockedNonce == userWithdrawalRequestNonce, so this must be strictly less than
if (nextNonce < userWithdrawalRequestNonce) {
return (0, false);
}

LidoWithdraw.claimWithdrawal(requestIds[0]);
WithdrawManager.completeWithdrawal(Deployments.ALT_ETH_ADDRESS);

tokensClaimed = address(this).balance;
(bool sent,) = vault.call{value: tokensClaimed}("");
Expand All @@ -109,8 +66,6 @@ contract KelpCooldownHolder is ClonedCoolDownHolder {
library KelpLib {
using TypeConvert for int256;

uint256 internal constant stETH_PRECISION = 1e18;

function _getValueOfWithdrawRequest(
uint256 totalVaultShares,
address borrowToken,
Expand Down Expand Up @@ -141,25 +96,8 @@ library KelpLib {

function _canFinalizeWithdrawRequest(uint256 requestId) internal view returns (bool) {
address holder = address(uint160(requestId));
if (!KelpCooldownHolder(payable(holder)).triggered()) return false;

uint256[] memory requestIds = LidoWithdraw.getWithdrawalRequests(holder);
ILidoWithdraw.WithdrawalRequestStatus[] memory withdrawsStatus = LidoWithdraw.getWithdrawalStatus(requestIds);

return withdrawsStatus[0].isFinalized;
}

function _canTriggerExtraStep(uint256 requestId) internal view returns (bool) {
address holder = address(uint160(requestId));
if (KelpCooldownHolder(payable(holder)).triggered()) return false;

(/* */, /* */, /* */, uint256 userWithdrawalRequestNonce) = WithdrawManager.getUserWithdrawalRequest(stETH, holder, 0);

return userWithdrawalRequestNonce < WithdrawManager.nextLockedNonce(stETH);
}

function _triggerExtraStep(uint256 requestId) internal {
KelpCooldownHolder holder = KelpCooldownHolder(payable(address(uint160(requestId))));
holder.triggerExtraStep();
(/* */, /* */, /* */, uint256 userWithdrawalRequestNonce) = WithdrawManager.getUserWithdrawalRequest(Deployments.ALT_ETH_ADDRESS, holder, 0);
uint256 nextNonce = WithdrawManager.nextLockedNonce(Deployments.ALT_ETH_ADDRESS);
return userWithdrawalRequestNonce < nextNonce;
}
}
4 changes: 3 additions & 1 deletion tests/BaseAcceptanceTest.sol
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,9 @@ abstract contract BaseAcceptanceTest is Test {
// NOTE: everything needs to run after create select fork
deployCodeTo("VaultRewarderLib.sol", Deployments.VAULT_REWARDER_LIB);
if (Deployments.CHAIN_ID == 1) {
vm.startPrank(0x22341fB5D92D3d801144aA5A925F401A91418A05);
if (FORK_BLOCK < 20492800) vm.startPrank(0x22341fB5D92D3d801144aA5A925F401A91418A05);
else vm.startPrank(Deployments.NOTIONAL.owner());

address tradingModule = address(new TradingModule(Deployments.NOTIONAL, Deployments.TRADING_MODULE));
// NOTE: fixes curve router
UUPSUpgradeable(address(Deployments.TRADING_MODULE)).upgradeTo(tradingModule);
Expand Down
14 changes: 14 additions & 0 deletions tests/Staking/PendlePT.t.sol.j2
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,20 @@ contract Test_PendlePT_{{ stakeSymbol }}_{{ primaryBorrowCurrency }} is BasePend
vm.prank(0x0EF8fa4760Db8f5Cd4d993f3e3416f30f942D705); // etherFi: admin
WithdrawRequestNFT.finalizeRequests(w.requestId);
}
{% elif contractName == 'PendlePTKelpVault' %}
function finalizeWithdrawRequest(address account) internal override {
// finalize withdraw request on Kelp
vm.deal(address(unstakingVault), 10_000e18);
vm.startPrank(0xCbcdd778AA25476F203814214dD3E9b9c46829A1); // kelp: operator
WithdrawManager.unlockQueue(
Deployments.ALT_ETH_ADDRESS,
type(uint256).max,
lrtOracle.getAssetPrice(Deployments.ALT_ETH_ADDRESS),
lrtOracle.rsETHPrice()
);
vm.stopPrank();
vm.roll(block.number + WithdrawManager.withdrawalDelayBlocks());
}
{% else %}
function finalizeWithdrawRequest(address account) internal override {}
{% endif %}
Expand Down
3 changes: 2 additions & 1 deletion tests/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,8 @@
"weETH": "0xE47F6c47DE1F1D93d8da32309D4dB90acDadeEaE",
'GHO': "0x3f12643D3f6f874d39C2a4c9f2Cd6f2DbAC877FC",
'USDe': "0xa569d910839Ae8865Da8F8e70FfFb0cBA869F961",
'ezETH': "0xCa140AE5a361b7434A729dCadA0ea60a50e249dd"
'ezETH': "0xCa140AE5a361b7434A729dCadA0ea60a50e249dd",
"rsETH": "0xb676EA4e0A54ffD579efFc1f1317C70d671f2028"
},
"arbitrum": {
"WETH": "0x639Fe6ab55C921f74e7fac1ee960C0B6293ba612",
Expand Down
120 changes: 17 additions & 103 deletions tests/generated/mainnet/PendlePT_rsETH_ETH.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
} from "@contracts/vaults/staking/protocols/PendlePrincipalToken.sol";
import {PendlePTOracle} from "@contracts/oracles/PendlePTOracle.sol";
import "@interfaces/chainlink/AggregatorV2V3Interface.sol";
import {WithdrawManager, stETH, LidoWithdraw, rsETH} from "@contracts/vaults/staking/protocols/Kelp.sol";
import {WithdrawManager, rsETH} from "@contracts/vaults/staking/protocols/Kelp.sol";
import {PendlePTKelpVault} from "@contracts/vaults/staking/PendlePTKelpVault.sol";

/**** NOTE: this file is not auto-generated because there is lots of custom withdraw logic *****/
Expand All @@ -26,7 +26,7 @@ address constant unstakingVault = 0xc66830E2667bc740c0BED9A71F18B14B8c8184bA;

contract Test_PendlePT_rsETH_ETH is BasePendleTest {
function setUp() public override {
FORK_BLOCK = 20033103;
FORK_BLOCK = 20492805;

harness = new Harness_PendlePT_rsETH_ETH();

Expand All @@ -41,45 +41,26 @@ contract Test_PendlePT_rsETH_ETH is BasePendleTest {
deleverageCollateralDecreaseRatio = 930;
defaultLiquidationDiscount = 955;
withdrawLiquidationDiscount = 945;
splitWithdrawPriceDecrease = 450;
borrowTokenPriceIncrease = 1500;

super.setUp();
}

function _finalizeFirstStep() private {
function finalizeWithdrawRequest(address /* account */) internal override {
// finalize withdraw request on Kelp
address stETHWhale = 0x804a7934bD8Cd166D35D8Fb5A1eb1035C8ee05ce;
vm.prank(stETHWhale);
IERC20(stETH).transfer(unstakingVault, 10_000e18);
vm.deal(address(unstakingVault), 10_000e18);
vm.startPrank(0xCbcdd778AA25476F203814214dD3E9b9c46829A1); // kelp: operator
WithdrawManager.unlockQueue(
address(stETH),
Deployments.ALT_ETH_ADDRESS,
type(uint256).max,
lrtOracle.getAssetPrice(address(stETH)),
lrtOracle.getAssetPrice(Deployments.ALT_ETH_ADDRESS),
lrtOracle.rsETHPrice()
);
vm.stopPrank();
vm.roll(block.number + WithdrawManager.withdrawalDelayBlocks());
}

function _finalizeSecondStep() private {
// finalize withdraw request on LIDO
address lidoAdmin = 0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84;
deal(lidoAdmin, 1000e18);
vm.startPrank(lidoAdmin);
LidoWithdraw.finalize{value: 1000e18}(LidoWithdraw.getLastRequestId(), 1.1687147788880494e27);
vm.stopPrank();
}

function finalizeWithdrawRequest(address account) internal override {
_finalizeFirstStep();

// trigger withdraw from Kelp nad unstake from LIDO
WithdrawRequest memory w = v().getWithdrawRequest(account);
PendlePTKelpVault(payable(address(vault))).triggerExtraStep(w.requestId);

_finalizeSecondStep();
}

function getDepositParams(
uint256 /* depositAmount */,
uint256 /* maturity */
Expand All @@ -102,20 +83,6 @@ contract Test_PendlePT_rsETH_ETH is BasePendleTest {
return abi.encode(d);
}

function test_deleverageAccount_splitAccountWithdrawRequest(
uint8 maturityIndex
) public override {
// list oracle
vm.startPrank(0x02479BFC7Dce53A02e26fE7baea45a0852CB0909);
Deployments.TRADING_MODULE.setPriceOracle(
address(rsETH),
AggregatorV2V3Interface(Harness_PendlePT_rsETH_ETH(address(harness)).baseToUSDOracle())
);
vm.stopPrank();

super.test_deleverageAccount_splitAccountWithdrawRequest(maturityIndex);
}

function test_exitVault_useWithdrawRequest_postExpiry(
uint8 maturityIndex, uint256 depositAmount, bool useForce
) public override {
Expand Down Expand Up @@ -157,57 +124,9 @@ contract Test_PendlePT_rsETH_ETH is BasePendleTest {
"Valuation and Deposit"
);
}

function test_canTriggerExtraStep(
uint8 maturityIndex, uint256 depositAmount, bool useForce
) public {
depositAmount = uint256(bound(depositAmount, minDeposit, maxDeposit));
maturityIndex = uint8(bound(maturityIndex, 0, 2));
address account = makeAddr("account");
uint256 maturity = maturities[maturityIndex];

uint256 vaultShares = enterVault(
account, depositAmount, maturity, getDepositParams(depositAmount, maturity)
);

setMaxOracleFreshness();
vm.warp(expires + 3600);
try Deployments.NOTIONAL.initializeMarkets(harness.getTestVaultConfig().borrowCurrencyId, false) {} catch {}
if (maturity < block.timestamp) {
// Push the vault shares to prime
totalVaultShares[maturity] -= vaultShares;
maturity = maturities[0];
totalVaultShares[maturity] += vaultShares;
}

if (useForce) {
_forceWithdraw(account);
} else {
vm.prank(account);
v().initiateWithdraw("");
}
WithdrawRequest memory w = v().getWithdrawRequest(account);

assertFalse(PendlePTKelpVault(payable(address(vault))).canTriggerExtraStep(w.requestId));
assertFalse(PendlePTKelpVault(payable(address(vault))).canFinalizeWithdrawRequest(w.requestId));

_finalizeFirstStep();

assertTrue(PendlePTKelpVault(payable(address(vault))).canTriggerExtraStep(w.requestId));
assertFalse(PendlePTKelpVault(payable(address(vault))).canFinalizeWithdrawRequest(w.requestId));

PendlePTKelpVault(payable(address(vault))).triggerExtraStep(w.requestId);

assertFalse(PendlePTKelpVault(payable(address(vault))).canTriggerExtraStep(w.requestId));
assertFalse(PendlePTKelpVault(payable(address(vault))).canFinalizeWithdrawRequest(w.requestId));

_finalizeSecondStep();

assertFalse(PendlePTKelpVault(payable(address(vault))).canTriggerExtraStep(w.requestId));
assertTrue(PendlePTKelpVault(payable(address(vault))).canFinalizeWithdrawRequest(w.requestId));
}
}


contract Harness_PendlePT_rsETH_ETH is PendleStakingHarness {
function getVaultName() public pure override returns (string memory) {
return 'Pendle:PT rsETH 27JUN2024:[ETH]';
Expand All @@ -216,8 +135,8 @@ contract Harness_PendlePT_rsETH_ETH is PendleStakingHarness {
function getRequiredOracles() public override view returns (
address[] memory token, address[] memory oracle
) {
token = new address[](2);
oracle = new address[](2);
token = new address[](3);
oracle = new address[](3);

// Custom PT Oracle
token[0] = ptAddress;
Expand All @@ -227,10 +146,9 @@ contract Harness_PendlePT_rsETH_ETH is PendleStakingHarness {
token[1] = 0x0000000000000000000000000000000000000000;
oracle[1] = 0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419;

// TODO: required in order to support withdraw requests
// rsETH
// token[2] = 0xA1290d69c65A6Fe4DF752f95823fae25cB99e5A7;
// oracle[2] = 0x150aab1C3D63a1eD0560B95F23d7905CE6544cCB;
token[2] = 0xA1290d69c65A6Fe4DF752f95823fae25cB99e5A7;
oracle[2] = 0xb676EA4e0A54ffD579efFc1f1317C70d671f2028;
}

function getTradingPermissions() public pure override returns (
Expand All @@ -250,25 +168,21 @@ contract Harness_PendlePT_rsETH_ETH is PendleStakingHarness {
return address(new PendlePTKelpVault(marketAddress, ptAddress));
}

function withdrawToken(address /* vault */) public pure override returns (address) {
return stETH;
}

constructor() {
marketAddress = 0x4f43c77872Db6BA177c270986CD30c3381AF37Ee;
ptAddress = 0xB05cABCd99cf9a73b19805edefC5f67CA5d1895E;
marketAddress = 0x6b4740722e46048874d84306B2877600ABCea3Ae;
ptAddress = 0x7bAf258049cc8B9A78097723dc19a8b103D4098F;
twapDuration = 15 minutes; // recommended 15 - 30 min
useSyOracleRate = true;
baseToUSDOracle = 0x150aab1C3D63a1eD0560B95F23d7905CE6544cCB;
tokenOutSy = 0xA1290d69c65A6Fe4DF752f95823fae25cB99e5A7;
borrowToken = 0x0000000000000000000000000000000000000000;
baseToUSDOracle = 0xb676EA4e0A54ffD579efFc1f1317C70d671f2028;

UniV3Adapter.UniV3SingleData memory u;
u.fee = 500; // 0.05 %
bytes memory exchangeData = abi.encode(u);
uint8 primaryDexId = uint8(DexId.UNISWAP_V3);

setMetadata(StakingMetadata(1, primaryDexId, exchangeData, false));
setMetadata(StakingMetadata(1, primaryDexId, exchangeData, true));
}

}

0 comments on commit de862c8

Please sign in to comment.