From c4a3a1f5dee396b65981055b5e6a4348bb412acb Mon Sep 17 00:00:00 2001 From: kyriediculous Date: Wed, 5 Feb 2025 14:43:02 +0100 Subject: [PATCH 1/4] add Kelp --- src/adapters/rsETH/IKelp.sol | 35 +++++++++++++++++ src/adapters/rsETH/RsETHAdapter.sol | 61 +++++++++++++++++++++++++++++ test/adapters/RsETHAdapter.t.sol | 37 +++++++++++++++++ 3 files changed, 133 insertions(+) create mode 100644 src/adapters/rsETH/IKelp.sol create mode 100644 src/adapters/rsETH/RsETHAdapter.sol create mode 100644 test/adapters/RsETHAdapter.t.sol diff --git a/src/adapters/rsETH/IKelp.sol b/src/adapters/rsETH/IKelp.sol new file mode 100644 index 0000000..9c3dffc --- /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) external; + function completeWithdrawal(address asset) 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..d513d40 --- /dev/null +++ b/src/adapters/rsETH/RsETHAdapter.sol @@ -0,0 +1,61 @@ +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"; + +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) { + 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..56830f3 --- /dev/null +++ b/test/adapters/RsETHAdapter.t.sol @@ -0,0 +1,37 @@ +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"; + +address constant RSETH_HOLDER = 0x43594da5d6A03b2137a04DF5685805C676dEf7cB; + +// 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")); + adapter = new RsETHAdapter(); + vm.startPrank(RSETH_HOLDER); + ERC20(RSETH_TOKEN).transfer(address(this), 10 ether); + vm.stopPrank(); + } + + function test_request_and_claim() public { + bytes memory data = adapter._delegatecall(abi.encodeWithSelector(adapter.requestWithdraw.selector, 10 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)); + } +} From 8d51e6ade4faf984433ab5be74849ea037810ebb Mon Sep 17 00:00:00 2001 From: kyriediculous Date: Wed, 5 Feb 2025 14:46:59 +0100 Subject: [PATCH 2/4] remove action --- .github/workflows/create.yml | 52 ------------------------------------ 1 file changed, 52 deletions(-) delete mode 100644 .github/workflows/create.yml 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 From 7dbbc77c263d988861166c1da4e195b27f3283d0 Mon Sep 17 00:00:00 2001 From: kyriediculous Date: Thu, 6 Feb 2025 09:15:30 +0100 Subject: [PATCH 3/4] Kelp: update function signatures to fix withdrawal calls --- src/adapters/Adapter.sol | 5 ++++- src/adapters/rsETH/IKelp.sol | 4 ++-- src/adapters/rsETH/RsETHAdapter.sol | 6 ++++-- test/adapters/RsETHAdapter.t.sol | 11 +++++++---- 4 files changed, 17 insertions(+), 9 deletions(-) diff --git a/src/adapters/Adapter.sol b/src/adapters/Adapter.sol index 385abda..61a73c7 100644 --- a/src/adapters/Adapter.sol +++ b/src/adapters/Adapter.sol @@ -1,12 +1,15 @@ pragma solidity >=0.8.25; +import { console } from "forge-std/console.sol"; + library AdapterDelegateCall { error AdapterDelegateCallFailed(string msg); 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); - + console.log("success %s", success); + console.log("returnData %s", string(returnData)); 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 index 9c3dffc..858378c 100644 --- a/src/adapters/rsETH/IKelp.sol +++ b/src/adapters/rsETH/IKelp.sol @@ -20,8 +20,8 @@ interface Withdrawals { external view returns (uint256 underlyingToReceive); - function initiateWithdrawal(address asset, uint256 rsETHUnstaked) external; - function completeWithdrawal(address asset) external; + 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); } diff --git a/src/adapters/rsETH/RsETHAdapter.sol b/src/adapters/rsETH/RsETHAdapter.sol index d513d40..d99eeb5 100644 --- a/src/adapters/rsETH/RsETHAdapter.sol +++ b/src/adapters/rsETH/RsETHAdapter.sol @@ -10,6 +10,7 @@ import { _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 @@ -28,6 +29,7 @@ contract RsETHAdapter is Adapter { } 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); @@ -35,14 +37,14 @@ contract RsETHAdapter is Adapter { 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); + 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); + w.completeWithdrawal(ETH_TOKEN, ""); amount = address(this).balance - balBefore; } diff --git a/test/adapters/RsETHAdapter.t.sol b/test/adapters/RsETHAdapter.t.sol index 56830f3..c45531d 100644 --- a/test/adapters/RsETHAdapter.t.sol +++ b/test/adapters/RsETHAdapter.t.sol @@ -6,8 +6,9 @@ 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 = 0x43594da5d6A03b2137a04DF5685805C676dEf7cB; +address constant RSETH_HOLDER = 0x22162DbBa43fE0477cdC5234E248264eC7C6EA7c; // tokenId 18143 // amountExpected 99999999999999999999 @@ -19,15 +20,17 @@ contract RsETHAdapterTest is Test, ERC721Receiver { using AdapterDelegateCall for RsETHAdapter; function setUp() public { - vm.createSelectFork(vm.envString("MAINNET_RPC")); + vm.createSelectFork(vm.envString("MAINNET_RPC"), 21_280_986); adapter = new RsETHAdapter(); vm.startPrank(RSETH_HOLDER); - ERC20(RSETH_TOKEN).transfer(address(this), 10 ether); + ERC20(RSETH_TOKEN).transfer(address(this), 6 ether); vm.stopPrank(); } function test_request_and_claim() public { - bytes memory data = adapter._delegatecall(abi.encodeWithSelector(adapter.requestWithdraw.selector, 10 ether)); + 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); From 7f58a328f2e836845c7092dd976154af55b8137d Mon Sep 17 00:00:00 2001 From: kyriediculous Date: Thu, 6 Feb 2025 15:13:30 +0100 Subject: [PATCH 4/4] remove console log --- src/adapters/Adapter.sol | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/adapters/Adapter.sol b/src/adapters/Adapter.sol index 61a73c7..68dd588 100644 --- a/src/adapters/Adapter.sol +++ b/src/adapters/Adapter.sol @@ -1,15 +1,11 @@ pragma solidity >=0.8.25; -import { console } from "forge-std/console.sol"; - library AdapterDelegateCall { error AdapterDelegateCallFailed(string msg); 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); - console.log("success %s", success); - console.log("returnData %s", string(returnData)); if (!success) { if (returnData.length < 4) { revert AdapterDelegateCallFailed("Unknown error occurred");