diff --git a/foundry/lib/IWETH.sol b/foundry/lib/IWETH.sol new file mode 100644 index 0000000..3b4a359 --- /dev/null +++ b/foundry/lib/IWETH.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.28; + +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +interface IWETH is IERC20 { + function deposit() external payable; + function withdraw(uint256) external; +} diff --git a/foundry/src/TychoRouter.sol b/foundry/src/TychoRouter.sol index f929e9b..5276748 100644 --- a/foundry/src/TychoRouter.sol +++ b/foundry/src/TychoRouter.sol @@ -1,6 +1,10 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.28; +import "../lib/IWETH.sol"; +import "../lib/bytes/LibPrefixLengthEncodedByteArray.sol"; +import "./CallbackVerificationDispatcher.sol"; +import "./SwapExecutionDispatcher.sol"; import "@openzeppelin/contracts/access/AccessControl.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; @@ -8,9 +12,12 @@ import "@permit2/src/interfaces/IAllowanceTransfer.sol"; import "./ExecutionDispatcher.sol"; import "./CallbackVerificationDispatcher.sol"; import "@openzeppelin/contracts/utils/Pausable.sol"; +import {Swap} from "./Swap.sol"; error TychoRouter__WithdrawalFailed(); error TychoRouter__AddressZero(); +error TychoRouter__NegativeSlippage(uint256 amount, uint256 minAmount); +error TychoRouter__MessageValueMismatch(uint256 value, uint256 amount); contract TychoRouter is AccessControl, @@ -19,8 +26,11 @@ contract TychoRouter is Pausable { IAllowanceTransfer public immutable permit2; + IWETH private immutable _weth; using SafeERC20 for IERC20; + using LibPrefixLengthEncodedByteArray for bytes; + using Swap for bytes; //keccak256("NAME_OF_ROLE") : save gas on deployment bytes32 public constant EXECUTOR_SETTER_ROLE = @@ -48,9 +58,10 @@ contract TychoRouter is ); event FeeSet(uint256 indexed oldFee, uint256 indexed newFee); - constructor(address _permit2) { + constructor(address _permit2, address weth) { permit2 = IAllowanceTransfer(_permit2); _grantRole(DEFAULT_ADMIN_ROLE, msg.sender); + _weth = IWETH(weth); } /** @@ -209,6 +220,27 @@ contract TychoRouter is } } + /** + * @dev Wraps a defined amount of ETH. + * @param amount of native ETH to wrap. + */ + function _wrapETH(uint256 amount) internal { + if (msg.value > 0 && msg.value != amount) { + revert TychoRouter__MessageValueMismatch(msg.value, amount); + } + _weth.deposit{value: amount}(); + } + + /** + * @dev Unwraps a defined amount of WETH. + * @param amount of WETH to unwrap. + */ + function _unwrapETH(uint256 amount) internal { + uint256 unwrapAmount = + amount == 0 ? _weth.balanceOf(address(this)) : amount; + _weth.withdraw(unwrapAmount); + } + /** * @dev Allows this contract to receive native token */ diff --git a/foundry/test/TychoRouter.t.sol b/foundry/test/TychoRouter.t.sol index 3d6f6d0..5458016 100644 --- a/foundry/test/TychoRouter.t.sol +++ b/foundry/test/TychoRouter.t.sol @@ -189,4 +189,28 @@ contract TychoRouterTest is TychoRouterTestSetup { tychoRouter.pause(); vm.stopPrank(); } + + function testWrapETH() public { + uint256 amount = 1 ether; + vm.deal(BOB, amount); + + vm.startPrank(BOB); + tychoRouter.wrapETH{value: amount}(amount); + vm.stopPrank(); + + assertEq(address(tychoRouter).balance, 0); + assertEq(IERC20(WETH_ADDR).balanceOf(address(tychoRouter)), amount); + } + + function testUnwrapETH() public { + uint256 amount = 1 ether; + deal(WETH_ADDR, address(tychoRouter), amount); + + vm.startPrank(BOB); + tychoRouter.unwrapETH(amount); + vm.stopPrank(); + + assertEq(address(tychoRouter).balance, amount); + assertEq(IERC20(WETH_ADDR).balanceOf(address(tychoRouter)), 0); + } } diff --git a/foundry/test/TychoRouterTestSetup.sol b/foundry/test/TychoRouterTestSetup.sol index 8958313..345f7f0 100644 --- a/foundry/test/TychoRouterTestSetup.sol +++ b/foundry/test/TychoRouterTestSetup.sol @@ -4,16 +4,32 @@ pragma solidity ^0.8.13; import "@src/TychoRouter.sol"; import "./Constants.sol"; import "./mock/MockERC20.sol"; +import {WETH} from "../lib/permit2/lib/solmate/src/tokens/WETH.sol"; + +contract TychoRouterExposed is TychoRouter { + constructor(address _permit2, address weth) TychoRouter(_permit2, weth) {} + + function wrapETH(uint256 amount) external payable { + return _wrapETH(amount); + } + + function unwrapETH(uint256 amount) external { + return _unwrapETH(amount); + } +} contract TychoRouterTestSetup is Test, Constants { - TychoRouter tychoRouter; + TychoRouterExposed tychoRouter; address executorSetter; address permit2Address = address(0x000000000022D473030F116dDEE9F6B43aC78BA3); MockERC20[] tokens; function setUp() public { + uint256 forkBlock = 21000000; + vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock); + vm.startPrank(ADMIN); - tychoRouter = new TychoRouter(permit2Address); + tychoRouter = new TychoRouterExposed(permit2Address, WETH_ADDR); tychoRouter.grantRole(keccak256("EXECUTOR_SETTER_ROLE"), BOB); tychoRouter.grantRole(keccak256("FUND_RESCUER_ROLE"), FUND_RESCUER); tychoRouter.grantRole(keccak256("FEE_SETTER_ROLE"), FEE_SETTER);