Skip to content
Draft
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
91 changes: 91 additions & 0 deletions src/wallet/EnsoWalletV2.sol
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 {
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);
}
}
102 changes: 102 additions & 0 deletions src/wallet/EnsoWalletV2Factory.sol
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 {
using LibClone for address;
using SafeERC20 for IERC20;

address public immutable implementation;

event EnsoWalletV2Deployed(address wallet, address indexed account);

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) {
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) {
return keccak256(abi.encode(account));
}
}
19 changes: 19 additions & 0 deletions src/wallet/interfaces/IEnsoWalletV2.sol
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);
}

81 changes: 81 additions & 0 deletions test/unit/concrete/wallet/ensowalletv2/EnsoWalletV2.t.sol
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();
}
}
68 changes: 68 additions & 0 deletions test/unit/concrete/wallet/ensowalletv2/execute.t.sol
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, "");
}
}

Loading
Loading