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
52 changes: 0 additions & 52 deletions .github/workflows/create.yml

This file was deleted.

1 change: 0 additions & 1 deletion src/adapters/Adapter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ library AdapterDelegateCall {
function _delegatecall(Adapter adapter, bytes memory data) internal returns (bytes memory) {
// solhint-disable-next-line avoid-low-level-calls
(bool success, bytes memory returnData) = address(adapter).delegatecall(data);

if (!success) {
if (returnData.length < 4) {
revert AdapterDelegateCallFailed("Unknown error occurred");
Expand Down
35 changes: 35 additions & 0 deletions src/adapters/rsETH/IKelp.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
pragma solidity ^0.8.25;

address constant ETH_TOKEN = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE;
address constant WITHDRAWALS = 0x62De59c08eB5dAE4b7E6F7a8cAd3006d6965ec16;
address constant DEPOSIT_POOL = 0x036676389e48133B63a802f8635AD39E752D375D;

struct WithdrawalRequest {
uint256 rsETHUnstaked;
uint256 expectedAssetAmount;
uint256 withdrawalStartBlock;
}

interface Withdrawals {
function nextUnusedNonce(address asset) external view returns (uint256);
function nextLockedNonce(address asset) external view returns (uint256);
function getExpectedAssetAmount(
address asset,
uint256 amount
)
external
view
returns (uint256 underlyingToReceive);
function initiateWithdrawal(address asset, uint256 rsETHUnstaked, string calldata referralId) external;
function completeWithdrawal(address asset, string calldata referralId) external;
function getAvailableAssetAmount(address asset) external view returns (uint256 availableAssetAmount);
function withdrawalRequests(bytes32 id) external view returns (WithdrawalRequest memory);
}

interface DepositPool {
function getTotalAssetDeposits(address asset) external view returns (uint256 totalAssetDeposit);
}

function _getRequestID(address asset, uint256 nonce) pure returns (bytes32) {
return keccak256(abi.encodePacked(asset, nonce));
}
63 changes: 63 additions & 0 deletions src/adapters/rsETH/RsETHAdapter.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
pragma solidity >=0.8.25;

import { Adapter } from "@/adapters/Adapter.sol";
import {
DEPOSIT_POOL,
DepositPool,
ETH_TOKEN,
WITHDRAWALS,
Withdrawals,
_getRequestID,
WithdrawalRequest
} from "@/adapters/rsETH/IKelp.sol";
import { SafeTransferLib } from "solady/utils/SafeTransferLib.sol";

address constant RSETH_TOKEN = 0xA1290d69c65A6Fe4DF752f95823fae25cB99e5A7;
uint256 constant MIN_AMOUNT = 5_000_000_000_000_000; // 0.005 ETH
uint256 constant MAX_AMOUNT = 1000 ether;

contract RsETHAdapter is Adapter {
struct WithdrawNonces {
address asset;
uint256 nonce;
}

mapping(bytes32 => WithdrawNonces) internal withdrawNonces;

function previewWithdraw(uint256 amount) external view override returns (uint256 amountExpected) {
return Withdrawals(WITHDRAWALS).getExpectedAssetAmount(ETH_TOKEN, amount);
}

function requestWithdraw(uint256 amount) external override returns (uint256 tokenId, uint256 amountExpected) {
SafeTransferLib.safeApprove(RSETH_TOKEN, WITHDRAWALS, amount);
Withdrawals w = Withdrawals(WITHDRAWALS);
uint256 nonce = w.nextUnusedNonce(ETH_TOKEN);
amountExpected = w.getExpectedAssetAmount(ETH_TOKEN, amount);
tokenId = uint256(_getRequestID(ETH_TOKEN, nonce));
withdrawNonces[bytes32(tokenId)] = WithdrawNonces(ETH_TOKEN, nonce);

// This call will revert if the amount of available ETH is less than the amount requested
w.initiateWithdrawal(ETH_TOKEN, amount, "");
}

function claimWithdraw(uint256 tokenId) external override returns (uint256 amount) {
isFinalized(tokenId);
Withdrawals w = Withdrawals(WITHDRAWALS);
uint256 balBefore = address(this).balance;
w.completeWithdrawal(ETH_TOKEN, "");
amount = address(this).balance - balBefore;
}

function isFinalized(uint256 tokenId) public view override returns (bool) {
uint256 nextLockedNonce = Withdrawals(WITHDRAWALS).nextLockedNonce(ETH_TOKEN);
return withdrawNonces[bytes32(tokenId)].nonce >= nextLockedNonce;
}

function totalStaked() external view override returns (uint256) {
return DepositPool(DEPOSIT_POOL).getTotalAssetDeposits(ETH_TOKEN);
}

function minMaxAmount() external pure override returns (uint256 min, uint256 max) {
return (MIN_AMOUNT, MAX_AMOUNT);
}
}
40 changes: 40 additions & 0 deletions test/adapters/RsETHAdapter.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
pragma solidity >=0.8.25;

import { Test, console } from "forge-std/Test.sol";
import { VmSafe } from "forge-std/Vm.sol";
import { RsETHAdapter, RSETH_TOKEN } from "@/adapters/rsETH/RsETHAdapter.sol";
import { ERC721Receiver } from "@/utils/ERC721Receiver.sol";
import { ERC20 } from "solady/tokens/ERC20.sol";
import { AdapterDelegateCall } from "@/adapters/Adapter.sol";
import { Withdrawals, WITHDRAWALS, ETH_TOKEN } from "@/adapters/rsETH/IKelp.sol";

address constant RSETH_HOLDER = 0x22162DbBa43fE0477cdC5234E248264eC7C6EA7c;

// tokenId 18143
// amountExpected 99999999999999999999
// totalStaked 1283949800110909723568459

contract RsETHAdapterTest is Test, ERC721Receiver {
RsETHAdapter adapter;

using AdapterDelegateCall for RsETHAdapter;

function setUp() public {
vm.createSelectFork(vm.envString("MAINNET_RPC"), 21_280_986);
adapter = new RsETHAdapter();
vm.startPrank(RSETH_HOLDER);
ERC20(RSETH_TOKEN).transfer(address(this), 6 ether);
vm.stopPrank();
}

function test_request_and_claim() public {
uint256 available = Withdrawals(WITHDRAWALS).getAvailableAssetAmount(ETH_TOKEN);
console.log("available ETH for withdrawals %", available);
bytes memory data = adapter._delegatecall(abi.encodeWithSelector(adapter.requestWithdraw.selector, 5 ether));
(uint256 tokenId, uint256 amountExpected) = abi.decode(data, (uint256, uint256));
console.log("tokenId %s", tokenId);
console.log("amountExpected %s", amountExpected);
console.log("totalStaked %s", adapter.totalStaked());
assertFalse(adapter.isFinalized(tokenId));
}
}
Loading