diff --git a/contracts/token/contracts/zetachain/UniversalToken.sol b/contracts/token/contracts/zetachain/UniversalToken.sol index b5ed54f..6dc4fe2 100644 --- a/contracts/token/contracts/zetachain/UniversalToken.sol +++ b/contracts/token/contracts/zetachain/UniversalToken.sol @@ -6,17 +6,15 @@ import "@zetachain/protocol-contracts/contracts/zevm/interfaces/UniversalContrac import "@zetachain/protocol-contracts/contracts/zevm/interfaces/IGatewayZEVM.sol"; import "@zetachain/protocol-contracts/contracts/zevm/GatewayZEVM.sol"; import {SwapHelperLib} from "@zetachain/toolkit/contracts/SwapHelperLib.sol"; +import {ERC20BurnableUpgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20BurnableUpgradeable.sol"; import {ERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; import {ERC20BurnableUpgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20BurnableUpgradeable.sol"; import {ERC20PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20PausableUpgradeable.sol"; -import {ERC20PermitUpgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20PermitUpgradeable.sol"; import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; -import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; -import {ERC20BurnableUpgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20BurnableUpgradeable.sol"; -import "../shared/UniversalTokenEvents.sol"; +import "./UniversalTokenCore.sol"; contract UniversalToken is Initializable, @@ -24,32 +22,9 @@ contract UniversalToken is ERC20BurnableUpgradeable, ERC20PausableUpgradeable, OwnableUpgradeable, - ERC20PermitUpgradeable, UUPSUpgradeable, - UniversalContract, - UniversalTokenEvents + UniversalTokenCore { - bool public constant isUniversal = true; - - GatewayZEVM public gateway; - address public uniswapRouter; - uint256 private _nextTokenId; - uint256 public gasLimitAmount; - - error TransferFailed(); - error Unauthorized(); - error InvalidAddress(); - error InvalidGasLimit(); - error ApproveFailed(); - error ZeroMsgValue(); - - mapping(address => address) public connected; - - modifier onlyGateway() { - if (msg.sender != address(gateway)) revert Unauthorized(); - _; - } - /// @custom:oz-upgrades-unsafe-allow constructor constructor() { _disableInitializers(); @@ -67,142 +42,7 @@ contract UniversalToken is __ERC20Burnable_init(); __Ownable_init(initialOwner); __UUPSUpgradeable_init(); - if (gatewayAddress == address(0) || uniswapRouterAddress == address(0)) - revert InvalidAddress(); - if (gas == 0) revert InvalidGasLimit(); - gateway = GatewayZEVM(gatewayAddress); - uniswapRouter = uniswapRouterAddress; - gasLimitAmount = gas; - } - - function setGasLimit(uint256 gas) external onlyOwner { - if (gas == 0) revert InvalidGasLimit(); - gasLimitAmount = gas; - } - - function setConnected( - address zrc20, - address contractAddress - ) external onlyOwner { - connected[zrc20] = contractAddress; - emit SetConnected(zrc20, contractAddress); - } - - function transferCrossChain( - address destination, - address receiver, - uint256 amount - ) public payable whenNotPaused { - if (msg.value == 0) revert ZeroMsgValue(); - if (receiver == address(0)) revert InvalidAddress(); - _burn(msg.sender, amount); - - (address gasZRC20, uint256 gasFee) = IZRC20(destination) - .withdrawGasFeeWithGasLimit(gasLimitAmount); - if (destination != gasZRC20) revert InvalidAddress(); - - address WZETA = gateway.zetaToken(); - - IWETH9(WZETA).deposit{value: msg.value}(); - IWETH9(WZETA).approve(uniswapRouter, msg.value); - - uint256 out = SwapHelperLib.swapTokensForExactTokens( - uniswapRouter, - WZETA, - gasFee, - gasZRC20, - msg.value - ); - - uint256 remaining = msg.value - out; - - if (remaining > 0) { - IWETH9(WZETA).withdraw(remaining); - (bool success, ) = msg.sender.call{value: remaining}(""); - if (!success) revert TransferFailed(); - } - - bytes memory message = abi.encode(receiver, amount, 0, msg.sender); - - CallOptions memory callOptions = CallOptions(gasLimitAmount, false); - - RevertOptions memory revertOptions = RevertOptions( - address(this), - true, - address(0), - abi.encode(amount, msg.sender), - gasLimitAmount - ); - - IZRC20(gasZRC20).approve(address(gateway), gasFee); - gateway.call( - abi.encodePacked(connected[destination]), - destination, - message, - callOptions, - revertOptions - ); - emit TokenTransfer(destination, receiver, amount); - } - - function mint(address to, uint256 amount) public onlyOwner whenNotPaused { - _mint(to, amount); - } - - function onCall( - MessageContext calldata context, - address zrc20, - uint256 amount, - bytes calldata message - ) external override onlyGateway { - if (context.sender != connected[zrc20]) revert Unauthorized(); - ( - address destination, - address receiver, - uint256 tokenAmount, - address sender - ) = abi.decode(message, (address, address, uint256, address)); - if (destination == address(0)) { - _mint(receiver, tokenAmount); - } else { - (address gasZRC20, uint256 gasFee) = IZRC20(destination) - .withdrawGasFeeWithGasLimit(gasLimitAmount); - if (destination != gasZRC20) revert InvalidAddress(); - uint256 out = SwapHelperLib.swapExactTokensForTokens( - uniswapRouter, - zrc20, - amount, - destination, - 0 - ); - if (!IZRC20(destination).approve(address(gateway), out)) { - revert ApproveFailed(); - } - gateway.withdrawAndCall( - abi.encodePacked(connected[destination]), - out - gasFee, - destination, - abi.encode(receiver, tokenAmount, out - gasFee, sender), - CallOptions(gasLimitAmount, false), - RevertOptions( - address(this), - true, - address(0), - abi.encode(tokenAmount, sender), - 0 - ) - ); - } - emit TokenTransferToDestination(destination, receiver, amount); - } - - function onRevert(RevertContext calldata context) external onlyGateway { - (uint256 amount, address sender) = abi.decode( - context.revertMessage, - (uint256, address) - ); - _mint(sender, amount); - emit TokenTransferReverted(sender, amount); + __UniversalTokenCore_init(gatewayAddress, gas, uniswapRouterAddress); } function pause() public onlyOwner { @@ -213,12 +53,14 @@ contract UniversalToken is _unpause(); } + function mint(address to, uint256 amount) public onlyOwner { + _mint(to, amount); + } + function _authorizeUpgrade( address newImplementation ) internal override onlyOwner {} - receive() external payable {} - // The following functions are overrides required by Solidity. function _update( @@ -228,4 +70,6 @@ contract UniversalToken is ) internal override(ERC20Upgradeable, ERC20PausableUpgradeable) { super._update(from, to, value); } + + receive() external payable {} } diff --git a/contracts/token/contracts/zetachain/UniversalTokenCore.sol b/contracts/token/contracts/zetachain/UniversalTokenCore.sol new file mode 100644 index 0000000..73ad132 --- /dev/null +++ b/contracts/token/contracts/zetachain/UniversalTokenCore.sol @@ -0,0 +1,178 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.26; + +import {ERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; +import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import "@zetachain/protocol-contracts/contracts/zevm/interfaces/UniversalContract.sol"; +import "@zetachain/protocol-contracts/contracts/zevm/interfaces/IGatewayZEVM.sol"; +import "@zetachain/protocol-contracts/contracts/zevm/interfaces/IWZETA.sol"; +import "@zetachain/protocol-contracts/contracts/zevm/GatewayZEVM.sol"; +import {SwapHelperLib} from "@zetachain/toolkit/contracts/SwapHelperLib.sol"; + +import "../shared/UniversalTokenEvents.sol"; + +abstract contract UniversalTokenCore is + UniversalContract, + ERC20Upgradeable, + OwnableUpgradeable, + UniversalTokenEvents +{ + bool public constant isUniversal = true; + + GatewayZEVM public gateway; + address public uniswapRouter; + uint256 public gasLimitAmount; + + error TransferFailed(); + error Unauthorized(); + error InvalidAddress(); + error InvalidGasLimit(); + error ApproveFailed(); + error ZeroMsgValue(); + + mapping(address => address) public connected; + + modifier onlyGateway() { + if (msg.sender != address(gateway)) revert Unauthorized(); + _; + } + + function __UniversalTokenCore_init( + address gatewayAddress, + uint256 gas, + address uniswapRouterAddress + ) internal { + if (gatewayAddress == address(0) || uniswapRouterAddress == address(0)) + revert InvalidAddress(); + if (gas == 0) revert InvalidGasLimit(); + gateway = GatewayZEVM(payable(gatewayAddress)); + uniswapRouter = uniswapRouterAddress; + gasLimitAmount = gas; + } + + function setGasLimit(uint256 gas) external onlyOwner { + if (gas == 0) revert InvalidGasLimit(); + gasLimitAmount = gas; + } + + function setConnected( + address zrc20, + address contractAddress + ) external onlyOwner { + connected[zrc20] = contractAddress; + emit SetConnected(zrc20, contractAddress); + } + + function transferCrossChain( + address destination, + address receiver, + uint256 amount + ) public payable { + if (msg.value == 0) revert ZeroMsgValue(); + if (receiver == address(0)) revert InvalidAddress(); + _burn(msg.sender, amount); + + (address gasZRC20, uint256 gasFee) = IZRC20(destination) + .withdrawGasFeeWithGasLimit(gasLimitAmount); + if (destination != gasZRC20) revert InvalidAddress(); + + address WZETA = gateway.zetaToken(); + + IWETH9(WZETA).deposit{value: msg.value}(); + IWETH9(WZETA).approve(uniswapRouter, msg.value); + + uint256 out = SwapHelperLib.swapTokensForExactTokens( + uniswapRouter, + WZETA, + gasFee, + gasZRC20, + msg.value + ); + + uint256 remaining = msg.value - out; + + if (remaining > 0) { + IWETH9(WZETA).withdraw(remaining); + (bool success, ) = msg.sender.call{value: remaining}(""); + if (!success) revert TransferFailed(); + } + + bytes memory message = abi.encode(receiver, amount, 0, msg.sender); + + CallOptions memory callOptions = CallOptions(gasLimitAmount, false); + + RevertOptions memory revertOptions = RevertOptions( + address(this), + true, + address(0), + abi.encode(amount, msg.sender), + gasLimitAmount + ); + + IZRC20(gasZRC20).approve(address(gateway), gasFee); + gateway.call( + abi.encodePacked(connected[destination]), + destination, + message, + callOptions, + revertOptions + ); + emit TokenTransfer(destination, receiver, amount); + } + + function onCall( + MessageContext calldata context, + address zrc20, + uint256 amount, + bytes calldata message + ) external override onlyGateway { + if (context.sender != connected[zrc20]) revert Unauthorized(); + ( + address destination, + address receiver, + uint256 tokenAmount, + address sender + ) = abi.decode(message, (address, address, uint256, address)); + if (destination == address(0)) { + _mint(receiver, tokenAmount); + } else { + (address gasZRC20, uint256 gasFee) = IZRC20(destination) + .withdrawGasFeeWithGasLimit(gasLimitAmount); + if (destination != gasZRC20) revert InvalidAddress(); + uint256 out = SwapHelperLib.swapExactTokensForTokens( + uniswapRouter, + zrc20, + amount, + destination, + 0 + ); + if (!IZRC20(destination).approve(address(gateway), out)) { + revert ApproveFailed(); + } + gateway.withdrawAndCall( + abi.encodePacked(connected[destination]), + out - gasFee, + destination, + abi.encode(receiver, tokenAmount, out - gasFee, sender), + CallOptions(gasLimitAmount, false), + RevertOptions( + address(this), + true, + address(0), + abi.encode(tokenAmount, sender), + 0 + ) + ); + } + emit TokenTransferToDestination(destination, receiver, amount); + } + + function onRevert(RevertContext calldata context) external onlyGateway { + (uint256 amount, address sender) = abi.decode( + context.revertMessage, + (uint256, address) + ); + _mint(sender, amount); + emit TokenTransferReverted(sender, amount); + } +}