-
Notifications
You must be signed in to change notification settings - Fork 3
EnsoWalletV2 #55
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
base: main
Are you sure you want to change the base?
EnsoWalletV2 #55
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,91 @@ | ||
| // SPDX-License-Identifier: GPL-3.0-only | ||
| pragma solidity ^0.8.20; | ||
|
|
||
| import { AbstractEnsoShortcuts } from "../AbstractEnsoShortcuts.sol"; | ||
| import { AbstractMultiSend } from "../AbstractMultiSend.sol"; | ||
| import { Withdrawable } from "../utils/Withdrawable.sol"; | ||
|
|
||
| import { Initializable } from "openzeppelin-contracts/proxy/utils/Initializable.sol"; | ||
|
|
||
| contract EnsoWalletV2 is AbstractMultiSend, AbstractEnsoShortcuts, Initializable, Withdrawable { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It misses inheriting from |
||
| string public constant VERSION = "1.0.0"; | ||
| address public factory; | ||
| address private _owner; | ||
|
|
||
| error InvalidSender(address sender); | ||
|
|
||
| modifier onlyOwner() { | ||
| _checkOwner(); | ||
| _; | ||
| } | ||
|
|
||
| /** | ||
| * @notice Initializes the wallet with an owner address | ||
| * @param owner_ The address that will own this wallet | ||
| */ | ||
| function initialize(address owner_) external initializer { | ||
| _owner = owner_; | ||
| // sender has to be the factory | ||
| factory = msg.sender; | ||
| } | ||
|
|
||
| /** | ||
| * @notice Executes an arbitrary call to a target contract | ||
| * @param target The address of the contract to call | ||
| * @param value The amount of native token to send with the call | ||
| * @param data The calldata to send to the target contract | ||
| * @return success Whether the call succeeded | ||
| */ | ||
| function execute( | ||
| address target, | ||
| uint256 value, | ||
| bytes memory data | ||
| ) | ||
| external | ||
| payable | ||
| onlyOwner | ||
| returns (bool success) | ||
| { | ||
| assembly { | ||
| success := call(gas(), target, value, add(data, 0x20), mload(data), 0, 0) | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * @notice Executes a shortcut | ||
| * @dev Can be called by owner or factory | ||
| * @param accountId The bytes32 value representing an API user | ||
| * @param requestId The bytes32 value representing an API request | ||
| * @param commands An array of bytes32 values that encode calls | ||
| * @param state An array of bytes that are used to generate call data for each command | ||
| * @return response Array of response data from each executed command | ||
| */ | ||
| function executeShortcut( | ||
| bytes32 accountId, | ||
| bytes32 requestId, | ||
| bytes32[] calldata commands, | ||
| bytes[] calldata state | ||
| ) | ||
| public | ||
| payable | ||
| override | ||
| returns (bytes[] memory response) | ||
| { | ||
| return super.executeShortcut(accountId, requestId, commands, state); | ||
| } | ||
|
|
||
| /// @notice Abstract override function to return owner | ||
| function owner() public view override returns (address) { | ||
| return _owner; | ||
| } | ||
|
|
||
| /// @notice Abstract override function to validate msg.sender | ||
| function _checkMsgSender() internal view override(AbstractEnsoShortcuts, AbstractMultiSend) { | ||
| if (msg.sender != factory && msg.sender != owner()) revert InvalidSender(msg.sender); | ||
| } | ||
|
|
||
| /// @notice Abstract override function to validate if sender is the owner | ||
| function _checkOwner() internal view override { | ||
| if (msg.sender != owner()) revert InvalidSender(msg.sender); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,102 @@ | ||
| // SPDX-License-Identifier: GPL-3.0-only | ||
| pragma solidity ^0.8.20; | ||
|
|
||
| import { Token, TokenType } from "../interfaces/IEnsoRouter.sol"; | ||
|
|
||
| import { IEnsoWalletV2 } from "./interfaces/IEnsoWalletV2.sol"; | ||
|
|
||
| import { IERC1155 } from "openzeppelin-contracts/token/ERC1155/IERC1155.sol"; | ||
| import { IERC20, SafeERC20 } from "openzeppelin-contracts/token/ERC20/utils/SafeERC20.sol"; | ||
| import { IERC721 } from "openzeppelin-contracts/token/ERC721/IERC721.sol"; | ||
| import { LibClone } from "solady/utils/LibClone.sol"; | ||
|
|
||
| contract EnsoWalletV2Factory { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd create the |
||
| using LibClone for address; | ||
| using SafeERC20 for IERC20; | ||
|
|
||
| address public immutable implementation; | ||
|
|
||
| event EnsoWalletV2Deployed(address wallet, address indexed account); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd index wallet too here just in case |
||
|
|
||
| error WrongMsgValue(uint256 value, uint256 expectedAmount); | ||
| error UnsupportedTokenType(TokenType tokenType); | ||
|
|
||
| constructor(address implementation_) { | ||
| implementation = implementation_; | ||
| } | ||
|
|
||
| function deploy(address account) external returns (address wallet) { | ||
| return _deploy(account); | ||
| } | ||
|
|
||
| function deployAndExecute( | ||
| Token calldata tokenIn, | ||
| bytes calldata data | ||
| ) | ||
| external | ||
| payable | ||
| returns (address wallet, bytes memory response) | ||
| { | ||
| return _deployAndExecute(tokenIn, data); | ||
| } | ||
|
|
||
| function getAddress(address account) external view returns (address) { | ||
| bytes32 salt = _getSalt(account); | ||
| return implementation.predictDeterministicAddress(salt, address(this)); | ||
| } | ||
|
|
||
| function _deployAndExecute( | ||
| Token calldata tokenIn, | ||
| bytes calldata data | ||
| ) | ||
| private | ||
| returns (address wallet, bytes memory response) | ||
| { | ||
| // strictly only msg.sender can deploy and execute | ||
| wallet = _deploy(msg.sender); | ||
| bool isNativeAsset = _transfer(tokenIn, wallet); | ||
| if (!isNativeAsset && msg.value != 0) revert WrongMsgValue(msg.value, 0); | ||
|
|
||
| bool success; | ||
| (success, response) = wallet.call{ value: msg.value }(data); | ||
| if (!success) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if (response.length > 0) {
assembly ("memory-safe") {
revert(add(0x20, response), mload(response))
}
}
revert ExecutionFailed(); // NOTE: this custom error should be createdThis no only bubbles up any existing revert reason (like you did but adding I reckon we should ideally update EnsoRouter with this change too
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also, we should probably add this low-level call response handling to (bool success, bytes memory response) = target.call{value: msg.value}(data);
if (!success) {
if (response.length > 0) {
assembly ("memory-safe") {
revert(add(0x20, response), mload(response))
}
}
revert ExecutionFailed(); // NOTE: this custom error should be created on the interface
} |
||
| assembly { | ||
| revert(add(response, 32), mload(response)) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| function _deploy(address account) private returns (address wallet) { | ||
| bytes32 salt = _getSalt(account); | ||
| wallet = implementation.predictDeterministicAddress(salt, address(this)); | ||
| if (wallet.code.length == 0) { | ||
| implementation.cloneDeterministic(salt); | ||
| IEnsoWalletV2(wallet).initialize(account); | ||
| emit EnsoWalletV2Deployed(wallet, account); | ||
| } | ||
| } | ||
|
|
||
| function _transfer(Token calldata token, address receiver) private returns (bool isNativeAsset) { | ||
| TokenType tokenType = token.tokenType; | ||
|
|
||
| if (tokenType == TokenType.ERC20) { | ||
| (IERC20 erc20, uint256 amount) = abi.decode(token.data, (IERC20, uint256)); | ||
| erc20.safeTransferFrom(msg.sender, receiver, amount); | ||
| } else if (tokenType == TokenType.Native) { | ||
| // no need to get amount, it will come from msg.value | ||
| isNativeAsset = true; | ||
| } else if (tokenType == TokenType.ERC721) { | ||
| (IERC721 erc721, uint256 tokenId) = abi.decode(token.data, (IERC721, uint256)); | ||
| erc721.safeTransferFrom(msg.sender, receiver, tokenId); | ||
| } else if (tokenType == TokenType.ERC1155) { | ||
| (IERC1155 erc1155, uint256 tokenId, uint256 amount) = abi.decode(token.data, (IERC1155, uint256, uint256)); | ||
| erc1155.safeTransferFrom(msg.sender, receiver, tokenId, amount, "0x"); | ||
| } else { | ||
| revert UnsupportedTokenType(tokenType); | ||
| } | ||
| } | ||
|
|
||
| function _getSalt(address account) internal pure returns (bytes32) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd replace |
||
| return keccak256(abi.encode(account)); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| // SPDX-License-Identifier: GPL-3.0-only | ||
| pragma solidity ^0.8.20; | ||
|
|
||
| interface IEnsoWalletV2 { | ||
| error InvalidSender(address sender); | ||
|
|
||
| function initialize(address owner_) external; | ||
|
|
||
| function executeShortcut( | ||
| bytes32 accountId, | ||
| bytes32 requestId, | ||
| bytes32[] calldata commands, | ||
| bytes[] calldata state | ||
| ) | ||
| external | ||
| payable | ||
| returns (bytes[] memory response); | ||
| } | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,81 @@ | ||
| // SPDX-License-Identifier: GPL-3.0-only | ||
| pragma solidity ^0.8.20; | ||
|
|
||
| import { EnsoWalletV2 } from "../../../../../src/wallet/EnsoWalletV2.sol"; | ||
| import { EnsoWalletV2Factory } from "../../../../../src/wallet/EnsoWalletV2Factory.sol"; | ||
|
|
||
| import { MockERC1155 } from "../../../../mocks/MockERC1155.sol"; | ||
| import { MockERC20 } from "../../../../mocks/MockERC20.sol"; | ||
| import { MockERC721 } from "../../../../mocks/MockERC721.sol"; | ||
|
|
||
| import { Test } from "forge-std-1.9.7/Test.sol"; | ||
| import { IERC1155 } from "openzeppelin-contracts/token/ERC1155/IERC1155.sol"; | ||
|
|
||
| abstract contract EnsoWalletV2_Unit_Concrete_Test is Test { | ||
| address payable internal constant EOA_1 = payable(0xE150e171dDf7ef6785e2c6fBBbE9eCd0f2f63682); | ||
| bytes32 internal constant EOA_1_PK = 0x74dc97524c0473f102953ebfe8bbec30f0e9cd304703ed7275c708921deaab3b; | ||
|
|
||
| address payable internal s_deployer; | ||
| address payable internal s_owner; | ||
| address payable internal s_user; | ||
| address payable internal s_account1; | ||
| address payable internal s_account2; | ||
|
|
||
| EnsoWalletV2 internal s_walletImplementation; | ||
| EnsoWalletV2Factory internal s_walletFactory; | ||
| EnsoWalletV2 internal s_wallet; | ||
|
|
||
| MockERC20 internal s_erc20; | ||
| MockERC721 internal s_erc721; | ||
| MockERC1155 internal s_erc1155; | ||
|
|
||
| function setUp() public virtual { | ||
| s_deployer = payable(vm.addr(1)); | ||
| vm.deal(s_deployer, 1000 ether); | ||
| vm.label(s_deployer, "Deployer"); | ||
|
|
||
| s_owner = payable(vm.addr(2)); | ||
| vm.deal(s_owner, 1000 ether); | ||
| vm.label(s_owner, "Owner"); | ||
|
|
||
| s_user = payable(vm.addr(3)); | ||
| vm.deal(s_user, 1000 ether); | ||
| vm.label(s_user, "User"); | ||
|
|
||
| s_account1 = payable(vm.addr(5)); | ||
| vm.deal(s_account1, 1000 ether); | ||
| vm.label(s_account1, "Account 1"); | ||
|
|
||
| s_account2 = payable(vm.addr(6)); | ||
| vm.deal(s_account2, 1000 ether); | ||
| vm.label(s_account2, "Account 2"); | ||
|
|
||
| vm.startPrank(s_deployer); | ||
|
|
||
| // Deploy implementation | ||
| s_walletImplementation = new EnsoWalletV2(); | ||
| vm.label(address(s_walletImplementation), "EnsoWalletV2Implementation"); | ||
|
|
||
| // Deploy factory | ||
| s_walletFactory = new EnsoWalletV2Factory(address(s_walletImplementation)); | ||
| vm.label(address(s_walletFactory), "EnsoWalletV2Factory"); | ||
|
|
||
| // Deploy mock tokens | ||
| s_erc20 = new MockERC20("Mock ERC20", "MERC20"); | ||
| vm.label(address(s_erc20), "MockERC20"); | ||
|
|
||
| s_erc721 = new MockERC721("Mock ERC721", "MERC721"); | ||
| vm.label(address(s_erc721), "MockERC721"); | ||
|
|
||
| s_erc1155 = new MockERC1155("Mock ERC1155"); | ||
| vm.label(address(s_erc1155), "MockERC1155"); | ||
|
|
||
| vm.stopPrank(); | ||
| } | ||
|
|
||
| function _deployWallet(address owner) internal returns (EnsoWalletV2 wallet) { | ||
| // vm.startPrank(s_factory); | ||
| wallet = EnsoWalletV2(payable(s_walletFactory.deploy(owner))); | ||
| // vm.stopPrank(); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| // SPDX-License-Identifier: GPL-3.0-only | ||
| pragma solidity ^0.8.20; | ||
|
|
||
| import { EnsoWalletV2 } from "../../../../../src/wallet/EnsoWalletV2.sol"; | ||
| import { EnsoWalletV2_Unit_Concrete_Test } from "./EnsoWalletV2.t.sol"; | ||
|
|
||
| contract Target { | ||
| function func() external pure returns (uint256) { | ||
| return 42; | ||
| } | ||
|
|
||
| function functionWithValue(uint256 value) external payable returns (uint256) { | ||
| return value; | ||
| } | ||
|
|
||
| function revert() external pure { | ||
| revert("Test revert"); | ||
| } | ||
| } | ||
|
|
||
| contract EnsoWalletV2_Execute_Unit_Concrete_Test is EnsoWalletV2_Unit_Concrete_Test { | ||
| Target internal s_target; | ||
|
|
||
| function setUp() public override { | ||
| super.setUp(); | ||
|
|
||
| s_target = new Target(); | ||
| vm.label(address(s_target), "Target"); | ||
|
|
||
| s_wallet = _deployWallet(s_owner); | ||
| } | ||
|
|
||
| function test_WhenValidCall() external { | ||
| // it should execute call successfully | ||
| vm.startPrank(s_owner); | ||
| bool success = s_wallet.execute(address(s_target), 0, abi.encodeWithSelector(Target.func.selector)); | ||
|
|
||
| assertTrue(success); | ||
| } | ||
|
|
||
| function test_WhenCallWithValue() external { | ||
| // it should execute call with value | ||
| uint256 value = 1 ether; | ||
|
|
||
| vm.startPrank(s_owner); | ||
| bool success = s_wallet.execute{ value: value }( | ||
| address(s_target), value, abi.encodeWithSelector(Target.functionWithValue.selector, value) | ||
| ); | ||
|
|
||
| assertTrue(success); | ||
| } | ||
|
|
||
| function test_RevertWhen_TargetReverts() external { | ||
| // it should revert when target call reverts | ||
| vm.startPrank(s_owner); | ||
| bool success = s_wallet.execute(address(s_target), 0, abi.encodeWithSelector(Target.revert.selector)); | ||
|
|
||
| assertFalse(success); | ||
| } | ||
|
|
||
| function test_RevertWhen_NotOwner() external { | ||
| // it should revert when not called by owner | ||
| vm.startPrank(s_user); | ||
| vm.expectRevert(abi.encodeWithSelector(EnsoWalletV2.InvalidSender.selector, s_user)); | ||
| s_wallet.execute(address(s_target), 0, ""); | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
pragma solidity ^0.8.28;(determined byAbstractMultiSend.sol.