diff --git a/.gas-snapshot b/.gas-snapshot index d01fc8a..0e36ace 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -1,22 +1,25 @@ -IETest:testCommandDepositETH() (gas: 155637) IETest:testCommandSendETH() (gas: 77576) -IETest:testCommandSendETHRawAddr() (gas: 75267) -IETest:testCommandStakeETH() (gas: 147090) -IETest:testCommandSwapDAI() (gas: 168609) -IETest:testCommandSwapETH() (gas: 233248) -IETest:testCommandSwapForETH() (gas: 175502) -IETest:testCommandSwapUSDC() (gas: 171665) +IETest:testCommandSendETHRawAddr() (gas: 75289) +IETest:testCommandStakeETH() (gas: 147120) +IETest:testCommandSwapDAI() (gas: 138671) +IETest:testCommandSwapETH() (gas: 185596) +IETest:testCommandSwapForETH() (gas: 145586) +IETest:testCommandSwapUSDC() (gas: 171751) IETest:testCommandSwapUSDCForWBTC() (gas: 196670) -IETest:testCommandUnstakeETH() (gas: 287967) -IETest:testCommandWithdrawETH() (gas: 290713) -IETest:testDeploy() (gas: 3703025) -IETest:testENSNameOwnership() (gas: 50674) -IETest:testIENameSetting() (gas: 11118) -IETest:testPreviewCommandSendDecimals() (gas: 111416) -IETest:testPreviewCommandSendUSDC() (gas: 70092) -IETest:testPreviewSend() (gas: 55972) -IETest:testPreviewSendCommand() (gas: 69618) -IETest:testPreviewSendCommandRawAddr() (gas: 66800) -IETest:testPreviewSendRawAddr() (gas: 29973) +IETest:testDeploy() (gas: 4000936) +IETest:testENSNameOwnership() (gas: 50696) +IETest:testPreviewCommandSendDecimals() (gas: 111460) +IETest:testPreviewCommandSendUSDC() (gas: 70114) +IETest:testPreviewSend() (gas: 56016) +IETest:testPreviewSendCommand() (gas: 69640) +IETest:testPreviewSendCommandRawAddr() (gas: 66822) +IETest:testPreviewSendRawAddr() (gas: 30017) +IETest:testTokenAliasSetting() (gas: 10964) +IETest:testTranslateCommand() (gas: 10531) +IETest:testTranslateExecuteSend0_0_1ETH() (gas: 29102) +IETest:testTranslateExecuteSend0_1ETH() (gas: 28475) +IETest:testTranslateExecuteSend10USDC() (gas: 26870) +IETest:testTranslateExecuteSend1ETH() (gas: 30784) +IETest:testTranslateExecuteSend1Wei() (gas: 32438) NAMITest:testFailRegister() (gas: 9532) NAMITest:testRegister() (gas: 59011) \ No newline at end of file diff --git a/lib/solady b/lib/solady index 0dde2a0..bfff552 160000 --- a/lib/solady +++ b/lib/solady @@ -1 +1 @@ -Subproject commit 0dde2a008d917aa8076f348eac2855edbe181cc0 +Subproject commit bfff552c0d282c15258cab9377a7d4c5247d0434 diff --git a/src/IE.sol b/src/IE.sol index 77f30ea..f451f3e 100644 --- a/src/IE.sol +++ b/src/IE.sol @@ -186,8 +186,7 @@ contract IE { _extractSend(normalized); (to, amount, token, callData, executeCallData) = previewSend(_to, _amount, _token); } else if ( - action == "swap" || action == "exchange" || action == "stake" || action == "deposit" - || action == "unstake" || action == "withdraw" + action == "swap" || action == "sell" || action == "exchange" || action == "stake" ) { ( string memory amountIn, @@ -254,7 +253,7 @@ contract IE { public view virtual - returns (bool) + returns (bool intentMatched) { (,,,,, bytes memory executeCallData) = previewCommand(intent); if (executeCallData.length != userOp.callData.length) return false; @@ -266,7 +265,7 @@ contract IE { public view virtual - returns (bool) + returns (bool intentMatched) { (,,,,, bytes memory executeCallData) = previewCommand(intent); if (executeCallData.length != userOp.callData.length) return false; @@ -336,8 +335,7 @@ contract IE { (string memory to, string memory amount, string memory token) = _extractSend(normalized); send(to, amount, token); } else if ( - action == "swap" || action == "exchange" || action == "stake" || action == "deposit" - || action == "unstake" || action == "withdraw" + action == "swap" || action == "sell" || action == "exchange" || action == "stake" ) { ( string memory amountIn, @@ -546,9 +544,14 @@ contract IE { /// ==================== COMMAND TRANSLATION ==================== /// - /// @dev Translates the `intent` for send action from the solution `callData` of a standard `execute()`. + /// @dev Translates an `intent` from raw `command()` calldata. + function translateCommand(bytes calldata callData) public pure returns (string memory intent) { + return string(callData[4:]); + } + + /// @dev Translates an `intent` for send action from the solution `callData` of standard `execute()`. /// note: The function selector technically doesn't need to be `execute()` but params should match. - function translate(bytes calldata callData) + function translateExecute(bytes calldata callData) public view virtual @@ -560,13 +563,19 @@ contract IE { if (value != 0) { return string( abi.encodePacked( - "send ", _toString(value / 10 ** 18), " ETH to 0x", _toAsciiString(target) + "send ", + _convertWeiToString(value, 18), + " ETH to 0x", + _toAsciiString(target) ) ); } - // The userOp `execute()` calldata must be a call to the ERC20 `transfer()` method. - if (bytes4(callData[132:136]) != IToken.transfer.selector) revert InvalidSelector(); + if ( + bytes4(callData[132:136]) != IToken.transfer.selector + && bytes4(callData[132:136]) != IToken.approve.selector + ) revert InvalidSelector(); + bool transfer = bytes4(callData[132:136]) == IToken.transfer.selector; (string memory token, uint256 decimals) = _returnTokenAliasConstants(target); if (bytes(token).length == 0) token = aliases[target]; @@ -575,8 +584,8 @@ contract IE { return string( abi.encodePacked( - "send ", - _toString(value / 10 ** decimals), + transfer ? "send " : "approve ", + _convertWeiToString(value, decimals), " ", token, " to 0x", @@ -594,44 +603,52 @@ contract IE { virtual returns (string memory intent) { - // The token calldata must be a call to the ERC20 `transfer()` method. - if (bytes4(tokenCalldata) != IToken.transfer.selector) revert InvalidSelector(); - - (string memory tokenAlias, uint256 decimals) = _returnTokenAliasConstants(token); - if (bytes(tokenAlias).length == 0) tokenAlias = aliases[token]; - if (decimals == 0) decimals = token.readDecimals(); // Sanity check. - (address target, uint256 value) = abi.decode(tokenCalldata[4:], (address, uint256)); - - return string( - abi.encodePacked( - "send ", - _toString(value / 10 ** decimals), - " ", - token, - " to 0x", - _toAsciiString(target) - ) - ); + unchecked { + if ( + bytes4(tokenCalldata) != IToken.transfer.selector + && bytes4(tokenCalldata) != IToken.approve.selector + ) revert InvalidSelector(); + bool transfer = bytes4(tokenCalldata) == IToken.transfer.selector; + (string memory tokenAlias, uint256 decimals) = _returnTokenAliasConstants(token); + if (bytes(tokenAlias).length == 0) tokenAlias = aliases[token]; + if (decimals == 0) decimals = token.readDecimals(); // Sanity check. + (address target, uint256 value) = abi.decode(tokenCalldata[4:], (address, uint256)); + + return string( + abi.encodePacked( + transfer ? "send " : "approve ", + _convertWeiToString(value, decimals), + " ", + token, + " to 0x", + _toAsciiString(target) + ) + ); + } } - /// @dev Translate ERC4337 userOp `callData` into readable send `intent`. + /// @dev Translate ERC4337 userOp `callData` into readable `intent`. function translateUserOp(UserOperation calldata userOp) public view virtual returns (string memory intent) { - return translate(userOp.callData); + return bytes4(userOp.callData) == IExecutor.execute.selector + ? translateExecute(userOp.callData) + : translateCommand(userOp.callData); } - /// @dev Translate packed ERC4337 userOp `callData` into readable send `intent`. + /// @dev Translate packed ERC4337 userOp `callData` into readable `intent`. function translatePackedUserOp(PackedUserOperation calldata userOp) public view virtual returns (string memory intent) { - return translate(userOp.callData); + return bytes4(userOp.callData) == IExecutor.execute.selector + ? translateExecute(userOp.callData) + : translateCommand(userOp.callData); } /// ================== BALANCE & SUPPLY HELPERS ================== /// @@ -927,6 +944,57 @@ contract IE { } } + /// @dev Convert number to string and insert decimal point. + function _convertWeiToString(uint256 weiAmount, uint256 decimals) + internal + pure + virtual + returns (string memory) + { + unchecked { + uint256 scalingFactor = 10 ** decimals; + + string memory wholeNumberStr = _toString(weiAmount / scalingFactor); + string memory decimalPartStr = _toString(weiAmount % scalingFactor); + + while (bytes(decimalPartStr).length != decimals) { + decimalPartStr = string(abi.encodePacked("0", decimalPartStr)); + } + + decimalPartStr = _removeTrailingZeros(decimalPartStr); + + if (bytes(decimalPartStr).length == 0) { + return wholeNumberStr; + } + + return string(abi.encodePacked(wholeNumberStr, ".", decimalPartStr)); + } + } + + /// @dev Remove any trailing zeroes from string. + function _removeTrailingZeros(string memory str) + internal + pure + virtual + returns (string memory) + { + unchecked { + bytes memory strBytes = bytes(str); + uint256 end = strBytes.length; + + while (end != 0 && strBytes[end - 1] == "0") { + --end; + } + + bytes memory trimmedBytes = new bytes(end); + for (uint256 i; i != end; ++i) { + trimmedBytes[i] = strBytes[i]; + } + + return string(trimmedBytes); + } + } + /// @dev Returns the base 10 decimal representation of `value`. /// Modified from (https://github.com/Vectorized/solady/blob/main/src/utils/LibString.sol) function _toString(uint256 value) internal pure virtual returns (string memory str) { @@ -949,8 +1017,9 @@ contract IE { } } -/// @dev Simple token transfer interface. +/// @dev Simple token handler interface. interface IToken { + function approve(address, uint256) external returns (bool); function transfer(address, uint256) external returns (bool); } diff --git a/test/IE.t.sol b/test/IE.t.sol index c9f9070..b7d2080 100644 --- a/test/IE.t.sol +++ b/test/IE.t.sol @@ -116,7 +116,7 @@ contract IETest is Test { assertEq(asset, ETH); } - function testIENameSetting() public payable { + function testTokenAliasSetting() public payable { assertEq(ie.tokens("usdc"), USDC); } @@ -138,29 +138,6 @@ contract IETest is Test { ie.command{value: 1 ether}("stake 1 eth into lido"); } - function testCommandDepositETH() public payable { - vm.prank(VITALIK_DOT_ETH); - ie.command{value: 1 ether}("deposit 1 eth into reth"); - } - - function testCommandWithdrawETH() public payable { - vm.prank(VITALIK_DOT_ETH); - ie.command{value: 1 ether}("deposit 1 eth into reth"); - vm.prank(VITALIK_DOT_ETH); - IERC20(RETH).approve(address(ie), 100 ether); - vm.prank(VITALIK_DOT_ETH); - ie.command("withdraw 0.8 reth into eth"); - } - - function testCommandUnstakeETH() public payable { - vm.prank(VITALIK_DOT_ETH); - ie.command{value: 1 ether}("stake 1 eth into reth"); - vm.prank(VITALIK_DOT_ETH); - IERC20(RETH).approve(address(ie), 100 ether); - vm.prank(VITALIK_DOT_ETH); - ie.command("unstake 0.8 reth for eth"); - } - function testCommandSwapForETH() public payable { uint256 startBalETH = DAI_WHALE.balance; uint256 startBalDAI = IERC20(DAI).balanceOf(DAI_WHALE); @@ -196,9 +173,69 @@ contract IETest is Test { assert(startBalWBTC < IERC20(WBTC).balanceOf(USDC_WHALE)); assertEq(startBalUSDC - 100 * 10 ** 6, IERC20(USDC).balanceOf(USDC_WHALE)); } + + function testTranslateCommand() public payable { + string memory intent = "send z0r0z 1 usdc"; + string memory ret = ie.translateCommand(abi.encodePacked(ie.command.selector, intent)); + assertEq(ret, intent); + } + + function testTranslateExecuteSend1ETH() public payable { + string memory intent = "send 1 ETH to 0x1c0aa8ccd568d90d61659f060d1bfb1e6f855a20"; + bytes4 sig = IExecutor.execute.selector; + bytes memory execData = abi.encode(Z0R0Z_DOT_ETH, 1 ether, ""); + execData = abi.encodePacked(sig, execData); + string memory ret = ie.translateExecute(execData); + assertEq(ret, intent); + } + + function testTranslateExecuteSend0_1ETH() public payable { + string memory intent = "send 0.1 ETH to 0x1c0aa8ccd568d90d61659f060d1bfb1e6f855a20"; + bytes4 sig = IExecutor.execute.selector; + bytes memory execData = abi.encode(Z0R0Z_DOT_ETH, 100000000000000000, ""); + execData = abi.encodePacked(sig, execData); + string memory ret = ie.translateExecute(execData); + assertEq(ret, intent); + } + + function testTranslateExecuteSend0_0_1ETH() public payable { + string memory intent = "send 0.01 ETH to 0x1c0aa8ccd568d90d61659f060d1bfb1e6f855a20"; + bytes4 sig = IExecutor.execute.selector; + bytes memory execData = abi.encode(Z0R0Z_DOT_ETH, 10000000000000000, ""); + execData = abi.encodePacked(sig, execData); + string memory ret = ie.translateExecute(execData); + assertEq(ret, intent); + } + + function testTranslateExecuteSend1Wei() public payable { + string memory intent = + "send 0.000000000000000001 ETH to 0x1c0aa8ccd568d90d61659f060d1bfb1e6f855a20"; + bytes4 sig = IExecutor.execute.selector; + bytes memory execData = abi.encode(Z0R0Z_DOT_ETH, 1, ""); + execData = abi.encodePacked(sig, execData); + string memory ret = ie.translateExecute(execData); + assertEq(ret, intent); + } + + function testTranslateExecuteSend10USDC() public payable { + string memory intent = "send 10 USDC to 0x1c0aa8ccd568d90d61659f060d1bfb1e6f855a20"; + bytes memory execData = abi.encodeWithSelector( + IExecutor.execute.selector, + USDC, + 0, + abi.encodeWithSelector(IERC20.transfer.selector, Z0R0Z_DOT_ETH, 10000000) + ); + string memory ret = ie.translateExecute(execData); + assertEq(ret, intent); + } } interface IERC20 { function approve(address, uint256) external; // unsafe lol. function balanceOf(address) external view returns (uint256); + function transfer(address, uint256) external returns (bool); +} + +interface IExecutor { + function execute(address, uint256, bytes calldata) external payable returns (bytes memory); }