diff --git a/README.md b/README.md index 03f92cd..0afb37c 100644 --- a/README.md +++ b/README.md @@ -11,8 +11,10 @@ Deployment addresses of WORM stack: #### Ethereum Mainnet -| Contract | Address | -| -------- | ------------------------------------------ | -| BETH | 0x5624344235607940d4d4EE76Bf8817d403EB9Cf8 | -| WORM | 0xfC9d98CdB3529F32cD7fb02d175547641e145B29 | -| Staking | 0x03d4702b51a98661B89dF5fcBe8C4baeF84C60B7 | +| Contract | Address | +| ------------ | ------------------------------------------ | +| BETH | 0x5624344235607940d4d4EE76Bf8817d403EB9Cf8 | +| WORM | 0xfC9d98CdB3529F32cD7fb02d175547641e145B29 | +| Staking | 0x03d4702b51a98661B89dF5fcBe8C4baeF84C60B7 | +| BETHToETH | 0xbA5A285806c343AaD955a40FE4b6e5e607B752b6 | +| BETHToERC20 | 0xa769B7B5A899132170d8237D45D0Bd6aCeDDD053 | \ No newline at end of file diff --git a/script/DeployBETHToERC20.s.sol b/script/DeployBETHToERC20.s.sol new file mode 100644 index 0000000..de35103 --- /dev/null +++ b/script/DeployBETHToERC20.s.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "forge-std/Script.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "src/hooks/BETHToERC20.sol"; +import {IWNativeToken} from "src/hooks/cypher-eth/IWNativeToken.sol"; + +contract DeployBETHToERC20 is Script { + // mainnet addresses + address constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; + address constant BETH = 0x5624344235607940d4d4EE76Bf8817d403EB9Cf8; + address constant cypherETHRouter = 0x20C5893f69F635f55b0367C519F3f95e59c0b0Ab; + address constant uniswapETHRouter = 0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45; + + function run() external { + vm.startBroadcast(); + + BETHToERC20 bethToERC20 = new BETHToERC20( + IERC20(BETH), IWNativeToken(WETH), ISwapRouter(cypherETHRouter), IV3SwapRouter(uniswapETHRouter) + ); + console.log("BETHToERC20 deployed to:", address(bethToERC20)); + + vm.stopBroadcast(); + } +} diff --git a/src/hooks/BETHToERC20.sol b/src/hooks/BETHToERC20.sol new file mode 100644 index 0000000..7ed2975 --- /dev/null +++ b/src/hooks/BETHToERC20.sol @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {IWNativeToken} from "src/hooks/cypher-eth/IWNativeToken.sol"; +import {ISwapRouter} from "src/hooks/cypher-eth/ISwapRouter.sol"; +import {IV3SwapRouter} from "src/hooks/uniswap/IV3SwapRouter.sol"; + +/// this contract swaps your BETH to WETH using cypherETH and then swaps your WETH to any ERC-20 with path you provide +/// for path use V3 path and not V2 +contract BETHToERC20 { + IERC20 public immutable bethContract; + IWNativeToken public immutable wethContract; + ISwapRouter public immutable cypherETHRouter; + IV3SwapRouter public immutable uniswapRouter; + + constructor( + IERC20 _bethContract, + IWNativeToken _wethContract, + ISwapRouter _cypherETHRouter, + IV3SwapRouter _uniswapRouter + ) { + require(address(_bethContract) != address(0), "Invalid BETH address"); + require(address(_wethContract) != address(0), "Invalid WETH address"); + require(address(_cypherETHRouter) != address(0), "Invalid WETH address"); + require(address(_uniswapRouter) != address(0), "Invalid WETH address"); + bethContract = _bethContract; + wethContract = _wethContract; + cypherETHRouter = _cypherETHRouter; + uniswapRouter = _uniswapRouter; + } + + function swapBethWithERC20(uint256 _bethAmountIn, bytes memory _path, address _recipient) public { + require(_bethAmountIn > 0, "Amount must be greater than 0"); + require(_recipient != address(0), "Invalid recipient"); + + require( + bethContract.transferFrom(msg.sender, address(this), _bethAmountIn), "error while transferFrom beth to this" + ); + + // BETH -> WETH + bethContract.approve(address(cypherETHRouter), _bethAmountIn); + uint256 wethAmount = cypherETHRouter.exactInputSingle( + ISwapRouter.ExactInputSingleParams({ + tokenIn: address(bethContract), + tokenOut: address(wethContract), + deployer: address(0), + recipient: address(this), + deadline: block.timestamp + 15 minutes, + amountIn: _bethAmountIn, + amountOutMinimum: 0, + limitSqrtPrice: 0 + }) + ); + bethContract.approve(address(cypherETHRouter), 0); + + // WETH -> ERC-20 + wethContract.approve(address(uniswapRouter), wethAmount); + uniswapRouter.exactInput( + IV3SwapRouter.ExactInputParams({ + path: _path, recipient: _recipient, amountIn: wethAmount, amountOutMinimum: 0 + }) + ); + wethContract.approve(address(uniswapRouter), 0); + } +} diff --git a/src/hooks/uniswap/IUniswapV3SwapCallback.sol b/src/hooks/uniswap/IUniswapV3SwapCallback.sol new file mode 100644 index 0000000..9886e4a --- /dev/null +++ b/src/hooks/uniswap/IUniswapV3SwapCallback.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity >=0.5.0; + +/// @title Callback for IUniswapV3PoolActions#swap +/// @notice Any contract that calls IUniswapV3PoolActions#swap must implement this interface +interface IUniswapV3SwapCallback { + /// @notice Called to `msg.sender` after executing a swap via IUniswapV3Pool#swap. + /// @dev In the implementation you must pay the pool tokens owed for the swap. + /// The caller of this method must be checked to be a UniswapV3Pool deployed by the canonical UniswapV3Factory. + /// amount0Delta and amount1Delta can both be 0 if no tokens were swapped. + /// @param amount0Delta The amount of token0 that was sent (negative) or must be received (positive) by the pool by + /// the end of the swap. If positive, the callback must send that amount of token0 to the pool. + /// @param amount1Delta The amount of token1 that was sent (negative) or must be received (positive) by the pool by + /// the end of the swap. If positive, the callback must send that amount of token1 to the pool. + /// @param data Any data passed through by the caller via the IUniswapV3PoolActions#swap call + function uniswapV3SwapCallback(int256 amount0Delta, int256 amount1Delta, bytes calldata data) external; +} diff --git a/src/hooks/uniswap/IV3SwapRouter.sol b/src/hooks/uniswap/IV3SwapRouter.sol new file mode 100644 index 0000000..1e33e87 --- /dev/null +++ b/src/hooks/uniswap/IV3SwapRouter.sol @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity >=0.7.5; +pragma abicoder v2; + +import "./IUniswapV3SwapCallback.sol"; + +/// @title Router token swapping functionality +/// @notice Functions for swapping tokens via Uniswap V3 +interface IV3SwapRouter is IUniswapV3SwapCallback { + struct ExactInputSingleParams { + address tokenIn; + address tokenOut; + uint24 fee; + address recipient; + uint256 amountIn; + uint256 amountOutMinimum; + uint160 sqrtPriceLimitX96; + } + + /// @notice Swaps `amountIn` of one token for as much as possible of another token + /// @dev Setting `amountIn` to 0 will cause the contract to look up its own balance, + /// and swap the entire amount, enabling contracts to send tokens before calling this function. + /// @param params The parameters necessary for the swap, encoded as `ExactInputSingleParams` in calldata + /// @return amountOut The amount of the received token + function exactInputSingle(ExactInputSingleParams calldata params) external payable returns (uint256 amountOut); + + struct ExactInputParams { + bytes path; + address recipient; + uint256 amountIn; + uint256 amountOutMinimum; + } + + /// @notice Swaps `amountIn` of one token for as much as possible of another along the specified path + /// @dev Setting `amountIn` to 0 will cause the contract to look up its own balance, + /// and swap the entire amount, enabling contracts to send tokens before calling this function. + /// @param params The parameters necessary for the multi-hop swap, encoded as `ExactInputParams` in calldata + /// @return amountOut The amount of the received token + function exactInput(ExactInputParams calldata params) external payable returns (uint256 amountOut); + + struct ExactOutputSingleParams { + address tokenIn; + address tokenOut; + uint24 fee; + address recipient; + uint256 amountOut; + uint256 amountInMaximum; + uint160 sqrtPriceLimitX96; + } + + /// @notice Swaps as little as possible of one token for `amountOut` of another token + /// that may remain in the router after the swap. + /// @param params The parameters necessary for the swap, encoded as `ExactOutputSingleParams` in calldata + /// @return amountIn The amount of the input token + function exactOutputSingle(ExactOutputSingleParams calldata params) external payable returns (uint256 amountIn); + + struct ExactOutputParams { + bytes path; + address recipient; + uint256 amountOut; + uint256 amountInMaximum; + } + + /// @notice Swaps as little as possible of one token for `amountOut` of another along the specified path (reversed) + /// that may remain in the router after the swap. + /// @param params The parameters necessary for the multi-hop swap, encoded as `ExactOutputParams` in calldata + /// @return amountIn The amount of the input token + function exactOutput(ExactOutputParams calldata params) external payable returns (uint256 amountIn); +} +