-
Notifications
You must be signed in to change notification settings - Fork 3
feat: add native token support to TransferERC20Hook #831
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
subhasishgoswami
wants to merge
7
commits into
dev
Choose a base branch
from
feat/transfer_erc20_hook_support_native_tokens
base: dev
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
fb09785
feat: add native token support to TransferERC20Hook
subhasishgoswami a287372
feat: add SingleTransferHook for native token transfers
subhasishgoswami 33cec6a
feat: new hook for transfer single token
subhasishgoswami c0fc982
feat: deployment for single hook
subhasishgoswami a0fa28b
chore: small fixes
supervaulter 4fc328b
chore: merged
supervaulter d2152b3
chore: small fix
supervaulter File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| {} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| {} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| {} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,110 @@ | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
| pragma solidity 0.8.30; | ||
|
|
||
| // external | ||
| import { BytesLib } from "../../vendor/BytesLib.sol"; | ||
| import { Execution } from "modulekit/accounts/erc7579/lib/ExecutionLib.sol"; | ||
| import { IERC20 } from "@openzeppelin/contracts/interfaces/IERC20.sol"; | ||
|
|
||
| // Superform | ||
| import { BaseHook } from "../BaseHook.sol"; | ||
| import { HookSubTypes } from "../../libraries/HookSubTypes.sol"; | ||
| import { ISuperHookResult, ISuperHookContextAware, ISuperHookInspector } from "../../interfaces/ISuperHook.sol"; | ||
|
|
||
| /// @title TransferHook | ||
| /// @author Superform Labs | ||
| /// @dev data has the following structure | ||
| /// @notice address token = BytesLib.toAddress(data, 0); | ||
| /// @notice address to = BytesLib.toAddress(data, 20); | ||
| /// @notice uint256 amount = BytesLib.toUint256(data, 40); | ||
| /// @notice bool usePrevHookAmount = _decodeBool(data, 72); | ||
| contract TransferHook is BaseHook, ISuperHookContextAware { | ||
| uint256 private constant USE_PREV_HOOK_AMOUNT_POSITION = 72; | ||
|
|
||
| /// @dev This is not a constant because some chains have different representations for the native token | ||
| /// https://github.com/d-xo/weird-erc20?tab=readme-ov-file#erc-20-representation-of-native-currency | ||
| address public immutable NATIVE_TOKEN; | ||
|
|
||
| constructor(address _nativeToken) BaseHook(HookType.NONACCOUNTING, HookSubTypes.TOKEN) { | ||
| NATIVE_TOKEN = _nativeToken; | ||
| } | ||
|
|
||
| /*////////////////////////////////////////////////////////////// | ||
| VIEW METHODS | ||
| //////////////////////////////////////////////////////////////*/ | ||
| /// @inheritdoc BaseHook | ||
| function _buildHookExecutions( | ||
| address prevHook, | ||
| address account, | ||
| bytes calldata data | ||
| ) | ||
| internal | ||
| view | ||
| override | ||
| returns (Execution[] memory executions) | ||
| { | ||
| address token = BytesLib.toAddress(data, 0); | ||
| address to = BytesLib.toAddress(data, 20); | ||
| uint256 amount = BytesLib.toUint256(data, 40); | ||
| bool usePrevHookAmount = _decodeBool(data, USE_PREV_HOOK_AMOUNT_POSITION); | ||
|
|
||
| if (usePrevHookAmount) { | ||
| amount = ISuperHookResult(prevHook).getOutAmount(account); | ||
| } | ||
|
|
||
| if (amount == 0) revert AMOUNT_NOT_VALID(); | ||
| if (token == address(0)) revert ADDRESS_NOT_VALID(); | ||
|
|
||
| // @dev no-revert-on-failure tokens are not supported | ||
| executions = new Execution[](1); | ||
| if (token == NATIVE_TOKEN) { | ||
| // For native token, send ETH directly to the recipient | ||
| executions[0] = Execution({ target: to, value: amount, callData: "" }); | ||
| } else { | ||
| // For ERC20 tokens, use the standard transfer | ||
| executions[0] = | ||
| Execution({ target: token, value: 0, callData: abi.encodeCall(IERC20.transfer, (to, amount)) }); | ||
| } | ||
| } | ||
|
|
||
| /*////////////////////////////////////////////////////////////// | ||
| EXTERNAL METHODS | ||
| //////////////////////////////////////////////////////////////*/ | ||
|
|
||
| /// @inheritdoc ISuperHookContextAware | ||
| function decodeUsePrevHookAmount(bytes memory data) external pure returns (bool) { | ||
| return _decodeBool(data, USE_PREV_HOOK_AMOUNT_POSITION); | ||
| } | ||
|
|
||
| /// @inheritdoc ISuperHookInspector | ||
| function inspect(bytes calldata data) external pure override returns (bytes memory) { | ||
| return abi.encodePacked( | ||
| BytesLib.toAddress(data, 0), //token | ||
| BytesLib.toAddress(data, 20) //to | ||
| ); | ||
| } | ||
|
|
||
| /*////////////////////////////////////////////////////////////// | ||
| INTERNAL METHODS | ||
| //////////////////////////////////////////////////////////////*/ | ||
| function _preExecute(address, address account, bytes calldata data) internal override { | ||
| _setOutAmount(_getBalance(data), account); | ||
| } | ||
|
|
||
| function _postExecute(address, address account, bytes calldata data) internal override { | ||
| _setOutAmount(_getBalance(data) - getOutAmount(account), account); | ||
| } | ||
|
|
||
| /*////////////////////////////////////////////////////////////// | ||
| PRIVATE METHODS | ||
| //////////////////////////////////////////////////////////////*/ | ||
| function _getBalance(bytes memory data) private view returns (uint256) { | ||
| address token = BytesLib.toAddress(data, 0); | ||
| address to = BytesLib.toAddress(data, 20); | ||
| if (token == NATIVE_TOKEN) { | ||
| return to.balance; | ||
| } else { | ||
| return IERC20(token).balanceOf(to); | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,144 @@ | ||
| // SPDX-License-Identifier: MIT | ||
| pragma solidity 0.8.30; | ||
|
|
||
| import { Execution } from "modulekit/accounts/erc7579/lib/ExecutionLib.sol"; | ||
| import { TransferHook } from "../../../../src/hooks/tokens/TransferHook.sol"; | ||
| import { ISuperHook } from "../../../../src/interfaces/ISuperHook.sol"; | ||
| import { MockERC20 } from "../../../mocks/MockERC20.sol"; | ||
| import { MockHook } from "../../../mocks/MockHook.sol"; | ||
| import { BaseHook } from "../../../../src/hooks/BaseHook.sol"; | ||
| import { Helpers } from "../../../utils/Helpers.sol"; | ||
| import { BytesLib } from "../../../../src/vendor/BytesLib.sol"; | ||
|
|
||
| contract TransferHookTest is Helpers { | ||
| using BytesLib for bytes; | ||
|
|
||
| TransferHook public hook; | ||
| address public NATIVE_TOKEN = address(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE); | ||
|
|
||
| address token; | ||
| address to; | ||
| uint256 amount; | ||
|
|
||
| function setUp() public { | ||
| MockERC20 _mockToken = new MockERC20("Mock Token", "MTK", 18); | ||
| token = address(_mockToken); | ||
|
|
||
| to = address(this); | ||
| amount = 1000; | ||
|
|
||
| hook = new TransferHook(NATIVE_TOKEN); | ||
| } | ||
|
|
||
| function test_Constructor() public view { | ||
| assertEq(uint256(hook.hookType()), uint256(ISuperHook.HookType.NONACCOUNTING)); | ||
| assertEq(hook.NATIVE_TOKEN(), NATIVE_TOKEN); | ||
| } | ||
|
|
||
| function test_UsePrevHookAmount() public view { | ||
| bytes memory data = _encodeData(token, true); | ||
| assertTrue(hook.decodeUsePrevHookAmount(data)); | ||
|
|
||
| data = _encodeData(token, false); | ||
| assertFalse(hook.decodeUsePrevHookAmount(data)); | ||
| } | ||
|
|
||
| function test_Build_ERC20() public view { | ||
| bytes memory data = _encodeData(token, false); | ||
| Execution[] memory executions = hook.build(address(0), address(0), data); | ||
| assertEq(executions.length, 3); | ||
| assertEq(executions[1].target, token); | ||
| assertEq(executions[1].value, 0); | ||
| assertGt(executions[1].callData.length, 0); | ||
| } | ||
|
|
||
| function test_Build_NativeToken() public view { | ||
| bytes memory data = _encodeData(NATIVE_TOKEN, false); | ||
| Execution[] memory executions = hook.build(address(0), address(0), data); | ||
| assertEq(executions.length, 3); | ||
| assertEq(executions[1].target, to); | ||
| assertEq(executions[1].value, amount); | ||
| assertEq(executions[1].callData.length, 0); | ||
| } | ||
|
|
||
| function test_Build_ERC20_WithPrevHook() public { | ||
| uint256 prevHookAmount = 2000; | ||
| address mockPrevHook = address(new MockHook(ISuperHook.HookType.INFLOW, token)); | ||
| MockHook(mockPrevHook).setOutAmount(prevHookAmount, address(this)); | ||
|
|
||
| bytes memory data = _encodeData(token, true); | ||
| Execution[] memory executions = hook.build(mockPrevHook, address(this), data); | ||
| assertEq(executions.length, 3); | ||
| assertEq(executions[1].target, token); | ||
| assertEq(executions[1].value, 0); | ||
| assertGt(executions[1].callData.length, 0); | ||
| } | ||
|
|
||
| function test_Build_NativeToken_WithPrevHook() public { | ||
| uint256 prevHookAmount = 2000; | ||
| address mockPrevHook = address(new MockHook(ISuperHook.HookType.INFLOW, NATIVE_TOKEN)); | ||
| MockHook(mockPrevHook).setOutAmount(prevHookAmount, address(this)); | ||
|
|
||
| bytes memory data = _encodeData(NATIVE_TOKEN, true); | ||
| Execution[] memory executions = hook.build(mockPrevHook, address(this), data); | ||
| assertEq(executions.length, 3); | ||
| assertEq(executions[1].target, to); | ||
| assertEq(executions[1].value, prevHookAmount); | ||
| assertEq(executions[1].callData.length, 0); | ||
| } | ||
|
|
||
| function test_Build_RevertIf_AddressZero() public { | ||
| address zeroToken = address(0); | ||
| vm.expectRevert(BaseHook.ADDRESS_NOT_VALID.selector); | ||
| hook.build(address(0), address(this), _encodeData(zeroToken, false)); | ||
| } | ||
|
|
||
| function test_Build_RevertIf_AmountZero() public { | ||
| uint256 zeroAmount = 0; | ||
| bytes memory data = abi.encodePacked(token, to, zeroAmount, false); | ||
| vm.expectRevert(BaseHook.AMOUNT_NOT_VALID.selector); | ||
| hook.build(address(0), address(this), data); | ||
| } | ||
|
|
||
| function test_PreAndPostExecute_ERC20() public { | ||
| _getTokens(token, address(to), amount); | ||
| hook.preExecute(address(0), address(this), _encodeData(token, false)); | ||
| assertEq(hook.getOutAmount(address(this)), amount); | ||
|
|
||
| hook.postExecute(address(0), address(this), _encodeData(token, false)); | ||
| assertEq(hook.getOutAmount(address(this)), 0); | ||
| } | ||
|
|
||
| function test_PreAndPostExecute_NativeToken() public { | ||
| // Deal native token to the 'to' address | ||
| vm.deal(to, amount); | ||
|
|
||
| hook.preExecute(address(0), address(this), _encodeData(NATIVE_TOKEN, false)); | ||
| assertEq(hook.getOutAmount(address(this)), amount); | ||
|
|
||
| hook.postExecute(address(0), address(this), _encodeData(NATIVE_TOKEN, false)); | ||
| assertEq(hook.getOutAmount(address(this)), 0); | ||
| } | ||
|
|
||
| function test_Inspector_ERC20() public view { | ||
| bytes memory data = _encodeData(token, false); | ||
| bytes memory argsEncoded = hook.inspect(data); | ||
| assertGt(argsEncoded.length, 0); | ||
|
|
||
| assertEq(BytesLib.toAddress(argsEncoded, 0), token); | ||
| assertEq(BytesLib.toAddress(argsEncoded, 20), to); | ||
| } | ||
|
|
||
| function test_Inspector_NativeToken() public view { | ||
| bytes memory data = _encodeData(NATIVE_TOKEN, false); | ||
| bytes memory argsEncoded = hook.inspect(data); | ||
| assertGt(argsEncoded.length, 0); | ||
|
|
||
| assertEq(BytesLib.toAddress(argsEncoded, 0), NATIVE_TOKEN); | ||
| assertEq(BytesLib.toAddress(argsEncoded, 20), to); | ||
| } | ||
|
|
||
| function _encodeData(address tokenAddress, bool usePrev) internal view returns (bytes memory) { | ||
| return abi.encodePacked(tokenAddress, to, amount, usePrev); | ||
| } | ||
| } | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.