diff --git a/.github/workflows/create.yml b/.github/workflows/create.yml deleted file mode 100644 index e0e9369..0000000 --- a/.github/workflows/create.yml +++ /dev/null @@ -1,52 +0,0 @@ -name: "Create" - -# The workflow will run only when the "Use this template" button is used -on: - create: - -jobs: - create: - # We only run this action when the repository isn't the template repository. References: - # - https://docs.github.com/en/actions/learn-github-actions/contexts - # - https://docs.github.com/en/actions/learn-github-actions/expressions - if: ${{ !github.event.repository.is_template }} - permissions: "write-all" - runs-on: "ubuntu-latest" - steps: - - name: "Check out the repo" - uses: "actions/checkout@v4" - - - name: "Update package.json" - env: - GITHUB_REPOSITORY_DESCRIPTION: ${{ github.event.repository.description }} - run: - ./.github/scripts/rename.sh "$GITHUB_REPOSITORY" "$GITHUB_REPOSITORY_OWNER" "$GITHUB_REPOSITORY_DESCRIPTION" - - - name: "Add rename summary" - run: | - echo "## Commit result" >> $GITHUB_STEP_SUMMARY - echo "✅ Passed" >> $GITHUB_STEP_SUMMARY - - - name: "Remove files not needed in the user's copy of the template" - run: | - rm -f "./.github/FUNDING.yml" - rm -f "./.github/scripts/rename.sh" - rm -f "./.github/workflows/create.yml" - - - name: "Add remove summary" - run: | - echo "## Remove result" >> $GITHUB_STEP_SUMMARY - echo "✅ Passed" >> $GITHUB_STEP_SUMMARY - - - name: "Update commit" - uses: "stefanzweifel/git-auto-commit-action@v4" - with: - commit_message: "feat: initial commit" - commit_options: "--amend" - push_options: "--force" - skip_fetch: true - - - name: "Add commit summary" - run: | - echo "## Commit result" >> $GITHUB_STEP_SUMMARY - echo "✅ Passed" >> $GITHUB_STEP_SUMMARY diff --git a/src/adapters/Adapter.sol b/src/adapters/Adapter.sol index 385abda..68dd588 100644 --- a/src/adapters/Adapter.sol +++ b/src/adapters/Adapter.sol @@ -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"); diff --git a/src/adapters/rsETH/IKelp.sol b/src/adapters/rsETH/IKelp.sol new file mode 100644 index 0000000..858378c --- /dev/null +++ b/src/adapters/rsETH/IKelp.sol @@ -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)); +} diff --git a/src/adapters/rsETH/RsETHAdapter.sol b/src/adapters/rsETH/RsETHAdapter.sol new file mode 100644 index 0000000..d99eeb5 --- /dev/null +++ b/src/adapters/rsETH/RsETHAdapter.sol @@ -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); + } +} diff --git a/test/adapters/RsETHAdapter.t.sol b/test/adapters/RsETHAdapter.t.sol new file mode 100644 index 0000000..c45531d --- /dev/null +++ b/test/adapters/RsETHAdapter.t.sol @@ -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)); + } +}