feat: Wrap and unwrap ETH

--- don't change below this line ---
ENG-4041 Took 2 hours 28 minutes

Took 14 seconds

Took 11 seconds

Took 2 minutes

Took 1 minute


Took 7 minutes
This commit is contained in:
Diana Carvalho
2025-01-24 15:59:12 +00:00
parent 7d35b8bb36
commit 3b2d9fcbdf
4 changed files with 84 additions and 3 deletions

9
foundry/lib/IWETH.sol Normal file
View File

@@ -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;
}

View File

@@ -1,6 +1,10 @@
// SPDX-License-Identifier: UNLICENSED // SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.28; 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/access/AccessControl.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
@@ -8,9 +12,12 @@ import "@permit2/src/interfaces/IAllowanceTransfer.sol";
import "./ExecutionDispatcher.sol"; import "./ExecutionDispatcher.sol";
import "./CallbackVerificationDispatcher.sol"; import "./CallbackVerificationDispatcher.sol";
import "@openzeppelin/contracts/utils/Pausable.sol"; import "@openzeppelin/contracts/utils/Pausable.sol";
import {Swap} from "./Swap.sol";
error TychoRouter__WithdrawalFailed(); error TychoRouter__WithdrawalFailed();
error TychoRouter__AddressZero(); error TychoRouter__AddressZero();
error TychoRouter__NegativeSlippage(uint256 amount, uint256 minAmount);
error TychoRouter__MessageValueMismatch(uint256 value, uint256 amount);
contract TychoRouter is contract TychoRouter is
AccessControl, AccessControl,
@@ -19,8 +26,11 @@ contract TychoRouter is
Pausable Pausable
{ {
IAllowanceTransfer public immutable permit2; IAllowanceTransfer public immutable permit2;
IWETH private immutable _weth;
using SafeERC20 for IERC20; using SafeERC20 for IERC20;
using LibPrefixLengthEncodedByteArray for bytes;
using Swap for bytes;
//keccak256("NAME_OF_ROLE") : save gas on deployment //keccak256("NAME_OF_ROLE") : save gas on deployment
bytes32 public constant EXECUTOR_SETTER_ROLE = bytes32 public constant EXECUTOR_SETTER_ROLE =
@@ -48,9 +58,10 @@ contract TychoRouter is
); );
event FeeSet(uint256 indexed oldFee, uint256 indexed newFee); event FeeSet(uint256 indexed oldFee, uint256 indexed newFee);
constructor(address _permit2) { constructor(address _permit2, address weth) {
permit2 = IAllowanceTransfer(_permit2); permit2 = IAllowanceTransfer(_permit2);
_grantRole(DEFAULT_ADMIN_ROLE, msg.sender); _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 * @dev Allows this contract to receive native token
*/ */

View File

@@ -189,4 +189,28 @@ contract TychoRouterTest is TychoRouterTestSetup {
tychoRouter.pause(); tychoRouter.pause();
vm.stopPrank(); 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);
}
} }

View File

@@ -4,16 +4,32 @@ pragma solidity ^0.8.13;
import "@src/TychoRouter.sol"; import "@src/TychoRouter.sol";
import "./Constants.sol"; import "./Constants.sol";
import "./mock/MockERC20.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 { contract TychoRouterTestSetup is Test, Constants {
TychoRouter tychoRouter; TychoRouterExposed tychoRouter;
address executorSetter; address executorSetter;
address permit2Address = address(0x000000000022D473030F116dDEE9F6B43aC78BA3); address permit2Address = address(0x000000000022D473030F116dDEE9F6B43aC78BA3);
MockERC20[] tokens; MockERC20[] tokens;
function setUp() public { function setUp() public {
uint256 forkBlock = 21000000;
vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock);
vm.startPrank(ADMIN); vm.startPrank(ADMIN);
tychoRouter = new TychoRouter(permit2Address); tychoRouter = new TychoRouterExposed(permit2Address, WETH_ADDR);
tychoRouter.grantRole(keccak256("EXECUTOR_SETTER_ROLE"), BOB); tychoRouter.grantRole(keccak256("EXECUTOR_SETTER_ROLE"), BOB);
tychoRouter.grantRole(keccak256("FUND_RESCUER_ROLE"), FUND_RESCUER); tychoRouter.grantRole(keccak256("FUND_RESCUER_ROLE"), FUND_RESCUER);
tychoRouter.grantRole(keccak256("FEE_SETTER_ROLE"), FEE_SETTER); tychoRouter.grantRole(keccak256("FEE_SETTER_ROLE"), FEE_SETTER);