diff --git a/src/ulysses-omnichain/VirtualAccount.sol b/src/ulysses-omnichain/VirtualAccount.sol index 8d1edf6..51f713a 100644 --- a/src/ulysses-omnichain/VirtualAccount.sol +++ b/src/ulysses-omnichain/VirtualAccount.sol @@ -10,7 +10,7 @@ import {ERC1155Receiver} from "@openzeppelin/contracts/token/ERC1155/utils/ERC11 import {IERC1155Receiver} from "@openzeppelin/contracts/token/ERC1155/IERC1155Receiver.sol"; import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; -import {IVirtualAccount, Call} from "./interfaces/IVirtualAccount.sol"; +import {IVirtualAccount, Call, PayableCall} from "./interfaces/IVirtualAccount.sol"; import {IRootPort} from "./interfaces/IRootPort.sol"; /// @title VirtualAccount - Contract for managing a virtual user account on the Root Chain @@ -54,22 +54,51 @@ contract VirtualAccount is IVirtualAccount, ERC1155Receiver { } /// @inheritdoc IVirtualAccount - function call(Call[] calldata calls) - external - override - requiresApprovedCaller - returns (uint256 blockNumber, bytes[] memory returnData) - { - blockNumber = block.number; - returnData = new bytes[](calls.length); - for (uint256 i = 0; i < calls.length; i++) { + function call(Call[] calldata calls) external override requiresApprovedCaller returns (bytes[] memory returnData) { + uint256 length = calls.length; + returnData = new bytes[](length); + + for (uint256 i = 0; i < length;) { bool success; Call calldata _call = calls[i]; - if (isContract(_call.target)) { - (success, returnData[i]) = _call.target.call(_call.callData); + + if (isContract(_call.target)) (success, returnData[i]) = _call.target.call(_call.callData); + + if (!success) revert CallFailed(); + + unchecked { + ++i; } + } + } + + /// @inheritdoc IVirtualAccount + function payableCall(PayableCall[] calldata calls) public payable returns (bytes[] memory returnData) { + uint256 valAccumulator; + uint256 length = calls.length; + returnData = new bytes[](length); + PayableCall calldata _call; + for (uint256 i = 0; i < length;) { + _call = calls[i]; + uint256 val = _call.value; + // Humanity will be a Type V Kardashev Civilization before this overflows - andreas + // ~ 10^25 Wei in existence << ~ 10^76 size uint fits in a uint256 + unchecked { + valAccumulator += val; + } + + bool success; + + if (isContract(_call.target)) (success, returnData[i]) = _call.target.call{value: val}(_call.callData); + if (!success) revert CallFailed(); + + unchecked { + ++i; + } } + // Finally, make sure the msg.value = SUM(call[0...i].value) + if (msg.value != valAccumulator) revert CallFailed(); } /*////////////////////////////////////////////////////////////// diff --git a/src/ulysses-omnichain/interfaces/IVirtualAccount.sol b/src/ulysses-omnichain/interfaces/IVirtualAccount.sol index 7c15986..7f315c9 100644 --- a/src/ulysses-omnichain/interfaces/IVirtualAccount.sol +++ b/src/ulysses-omnichain/interfaces/IVirtualAccount.sol @@ -3,16 +3,23 @@ pragma solidity ^0.8.0; import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; -/// @notice Interface for the `Multicall2` contract. +/// @notice Call structure based off `Multicall2` contract for aggregating calls. struct Call { address target; bytes callData; } +/// @notice Payable call structure based off `Multicall3` contract for aggreagating calls with `msg.value`. +struct PayableCall { + address target; + bytes callData; + uint256 value; +} + /** * @title Virtual Account Contract * @notice A Virtual Account allows users to manage assets and perform interactions remotely while allowing dApps to keep encapsulated user balance for accounting purposes. - * @dev This contract is based off Maker's `Multicall2` contract, executes a set of `Call` objects if any of the performed calls is invalid the whole batch should revert. + * @dev This contract is based off `Multicall2` and `Multicall3` contract, executes a set of `Call` or `PayableCall` objects if any of the performed calls is invalid the whole batch should revert. */ interface IVirtualAccount is IERC721Receiver { /** @@ -48,10 +55,19 @@ interface IVirtualAccount is IERC721Receiver { function withdrawERC721(address _token, uint256 _tokenId) external; /** - * @notice + * @notice Aggregate calls ensuring each call is successful. Inspired by `Multicall2` contract. * @param callInput The call to make. + * @return The return data of the call. + */ + function call(Call[] calldata callInput) external returns (bytes[] memory); + + /** + * @notice Aggregate calls with a msg value ensuring each call is successful. Inspired by `Multicall3` contract. + * @param calls The calls to make. + * @return The return data of the calls. + * @dev Reverts if msg.value is less than the sum of the call values. */ - function call(Call[] calldata callInput) external returns (uint256 blockNumber, bytes[] memory); + function payableCall(PayableCall[] calldata calls) external payable returns (bytes[] memory); /*/////////////////////////////////////////////////////////////// ERRORS diff --git a/test/ulysses-omnichain/RootTest.t.sol b/test/ulysses-omnichain/RootTest.t.sol index 7b46ebf..335f05e 100644 --- a/test/ulysses-omnichain/RootTest.t.sol +++ b/test/ulysses-omnichain/RootTest.t.sol @@ -31,6 +31,8 @@ import {RootBridgeAgentFactory} from "@omni/factories/RootBridgeAgentFactory.sol import {BranchBridgeAgentFactory} from "@omni/factories/BranchBridgeAgentFactory.sol"; import {ArbitrumBranchBridgeAgentFactory} from "@omni/factories/ArbitrumBranchBridgeAgentFactory.sol"; +import {VirtualAccount, PayableCall} from "@omni/VirtualAccount.sol"; + //UTILS import {DepositParams, DepositMultipleParams} from "./mocks/MockRootBridgeAgent.t.sol"; import {Deposit, DepositStatus, DepositMultipleInput, DepositInput} from "@omni/interfaces/IBranchBridgeAgent.sol"; @@ -1414,6 +1416,48 @@ contract RootTest is DSTestPlus { ); } + function testPayableCall() public { + // Set up + testAddLocalTokenArbitrum(); + + // Prepare data + PayableCall[] memory calls = new PayableCall[](1); + + // Mock Omnichain dApp call + calls[0] = PayableCall({ + target: arbitrumWrappedNativeToken, + callData: abi.encodeWithSelector(bytes4(0xd0e30db0)), + value: 1 ether + }); + + // Prank into MulticallBridgeAgent + hevm.startPrank(address(multicallBridgeAgent)); + + // Get User Virtual Account + VirtualAccount userAccount = RootPort(address(rootPort)).fetchVirtualAccount(address(this)); + + // Toggle Router Virtual Account use for tx execution + RootPort(address(rootPort)).toggleVirtualAccountApproved(userAccount, address(rootMulticallRouter)); + + hevm.stopPrank(); + + //Prank into MulticallRootRouter + hevm.startPrank(address(rootMulticallRouter)); + + // Get some gas. + hevm.deal(address(rootMulticallRouter), 1 ether); + + // Call Deposit function + userAccount.payableCall{value: 1 ether}(calls); + + console2.log("Virtual Account Balance:", MockERC20(arbitrumWrappedNativeToken).balanceOf(address(userAccount))); + + require( + MockERC20(arbitrumWrappedNativeToken).balanceOf(address(userAccount)) == 1 ether, + "User should have 1 wrapped token" + ); + } + ////////////////////////////////////////////////////////////////////////// HELPERS /////////////////////////////////////////////////////////////////// function testCreateDepositSingle(