Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: initialize a prototypes #162

Merged
merged 33 commits into from
Jul 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
1f2625e
initialize gateway
lumtis Jun 18, 2024
bd71007
add receiver example
lumtis Jun 18, 2024
3553a8f
add test examples
lumtis Jun 18, 2024
791834b
add entry for test prototype
lumtis Jun 18, 2024
c1bd47d
typechain
lumtis Jun 18, 2024
6aed43c
change B to non-payable
lumtis Jun 18, 2024
8a973c1
generate
lumtis Jun 18, 2024
d562400
initialize custody contract
lumtis Jun 19, 2024
555ff9d
add execute with erc20 in gateway
lumtis Jun 19, 2024
21b8ace
add test
lumtis Jun 19, 2024
484096a
add some more tests
lumtis Jun 19, 2024
a0c8e5a
initalize uniswap test
lumtis Jun 19, 2024
5b893d2
finish test for uniswap
lumtis Jun 19, 2024
776caef
generate
lumtis Jun 19, 2024
704382b
use custom error
lumtis Jun 19, 2024
5e4766c
add some more comment
lumtis Jun 19, 2024
ce87491
a bit more comments
lumtis Jun 19, 2024
049f956
Merge branch 'main' into init-prototypes
skosito Jun 20, 2024
278c327
chore: change Gateway contract to be upgradable (#175)
skosito Jun 26, 2024
9303311
conflicts
lumtis Jun 27, 2024
cb0ca6c
fix yarn.lock
lumtis Jun 27, 2024
b9e5bfd
feat: inbound evm prototype (#178)
skosito Jun 27, 2024
9a785bc
feat: prototype inbound zevm (#183)
skosito Jul 1, 2024
430992a
feat: prototype outbound zevm (#191)
skosito Jul 1, 2024
4ee9023
Merge branch 'main' into init-prototypes
skosito Jul 1, 2024
0b1f894
fix coderabbit comments
skosito Jul 2, 2024
7db93dd
fix yarn generate
skosito Jul 2, 2024
69d5763
Merge branch 'main' into init-prototypes
skosito Jul 2, 2024
2b64c79
Update contracts/prototypes/evm/GatewayEVM.sol
lumtis Jul 3, 2024
ea767f6
adding access control TODOs
lumtis Jul 3, 2024
725da8e
PR comments
skosito Jul 3, 2024
efd62ce
add more checks to withdraw function
skosito Jul 3, 2024
0ff9029
Merge branch 'main' into init-prototypes
skosito Jul 3, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions contracts/prototypes/evm/ERC20CustodyNew.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.7;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "./interfaces.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";

// As the current version, ERC20CustodyNew hold the ERC20s deposited on ZetaChain
// This version include a functionality allowing to call a contract
// ERC20Custody doesn't call smart contract directly, it passes through the Gateway contract
contract ERC20CustodyNew is ReentrancyGuard{
using SafeERC20 for IERC20;
error ZeroAddress();

IGatewayEVM public gateway;

event Withdraw(address indexed token, address indexed to, uint256 amount);
event WithdrawAndCall(address indexed token, address indexed to, uint256 amount, bytes data);

constructor(address _gateway) {
if (_gateway == address(0)) {
revert ZeroAddress();
}
gateway = IGatewayEVM(_gateway);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: add a zero address validation

}

// Withdraw is called by TSS address, it directly transfers the tokens to the destination address without contract call
// TODO: Finalize access control
// https://github.com/zeta-chain/protocol-contracts/issues/204
function withdraw(address token, address to, uint256 amount) external nonReentrant {
IERC20(token).safeTransfer(to, amount);

emit Withdraw(token, to, amount);
}

// WithdrawAndCall is called by TSS address, it transfers the tokens and call a contract
// For this, it passes through the Gateway contract, it transfers the tokens to the Gateway contract and then calls the contract
// TODO: Finalize access control
// https://github.com/zeta-chain/protocol-contracts/issues/204
function withdrawAndCall(address token, address to, uint256 amount, bytes calldata data) external nonReentrant {
// Transfer the tokens to the Gateway contract
IERC20(token).safeTransfer(address(gateway), amount);

// Forward the call to the Gateway contract
gateway.executeWithERC20(token, to, amount, data);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

executeWithERC20 will happen always, even if the previous transfer to the gateway failed; is it a valid scenario?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No it should fail in this case
So if transfer returns a boolean, we need to check the returned value

Copy link
Contributor

@skosito skosito Jul 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we use safeTransfer instead?
EDIT: will change to safeTransfer because that seems cleaner than always checking response bool value but let me know your thoughts


emit WithdrawAndCall(token, to, amount, data);
}
}
145 changes: 145 additions & 0 deletions contracts/prototypes/evm/GatewayEVM.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.7;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";

// The GatewayEVM contract is the endpoint to call smart contracts on external chains
// The contract doesn't hold any funds and should never have active allowances
contract GatewayEVM is Initializable, OwnableUpgradeable, UUPSUpgradeable {
using SafeERC20 for IERC20;

error ExecutionFailed();
error DepositFailed();
error InsufficientETHAmount();
error InsufficientERC20Amount();
error ZeroAddress();
error ApprovalFailed();

address public custody;
address public tssAddress;

event Executed(address indexed destination, uint256 value, bytes data);
event ExecutedWithERC20(address indexed token, address indexed to, uint256 amount, bytes data);
event Deposit(address indexed sender, address indexed receiver, uint256 amount, address asset, bytes payload);
event Call(address indexed sender, address indexed receiver, bytes payload);

/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers();
}

function initialize(address _tssAddress) public initializer {
__Ownable_init();
__UUPSUpgradeable_init();

if (_tssAddress == address(0)) revert ZeroAddress();

tssAddress = _tssAddress;
}

function _authorizeUpgrade(address newImplementation) internal override onlyOwner() {}

function _execute(address destination, bytes calldata data) internal returns (bytes memory) {
(bool success, bytes memory result) = destination.call{value: msg.value}(data);

if (!success) revert ExecutionFailed();

return result;
}

// Called by the TSS
// Execution without ERC20 tokens, it is payable and can be used in the case of WithdrawAndCall for Gas ZRC20
// It can be also used for contract call without asset movement
function execute(address destination, bytes calldata data) external payable returns (bytes memory) {
bytes memory result = _execute(destination, data);

emit Executed(destination, msg.value, data);

return result;
}

// Called by the ERC20Custody contract
// It call a function using ERC20 transfer
// Since the goal is to allow calling contract not designed for ZetaChain specifically, it uses ERC20 allowance system
// It provides allowance to destination contract and call destination contract. In the end, it remove remaining allowance and transfer remaining tokens back to the custody contract for security purposes
function executeWithERC20(
address token,
address to,
uint256 amount,
bytes calldata data
) external returns (bytes memory) {
if (amount == 0) revert InsufficientETHAmount();
// Approve the target contract to spend the tokens
if(!resetApproval(token, to)) revert ApprovalFailed();
if(!IERC20(token).approve(to, amount)) revert ApprovalFailed();

// Execute the call on the target contract
bytes memory result = _execute(to, data);

// Reset approval
if(!resetApproval(token, to)) revert ApprovalFailed();

// Transfer any remaining tokens back to the custody contract
uint256 remainingBalance = IERC20(token).balanceOf(address(this));
if (remainingBalance > 0) {
IERC20(token).safeTransfer(address(custody), remainingBalance);
}

emit ExecutedWithERC20(token, to, amount, data);

return result;
}

// Deposit ETH to tss
function deposit(address receiver) external payable {
if (msg.value == 0) revert InsufficientETHAmount();
(bool deposited, ) = tssAddress.call{value: msg.value}("");

if (deposited == false) revert DepositFailed();

emit Deposit(msg.sender, receiver, msg.value, address(0), "");
}

// Deposit ERC20 tokens to custody
function deposit(address receiver, uint256 amount, address asset) external {
if (amount == 0) revert InsufficientERC20Amount();
IERC20(asset).safeTransferFrom(msg.sender, address(custody), amount);

emit Deposit(msg.sender, receiver, amount, asset, "");
}

// Deposit ETH to tss and call an omnichain smart contract
function depositAndCall(address receiver, bytes calldata payload) external payable {
if (msg.value == 0) revert InsufficientETHAmount();
(bool deposited, ) = tssAddress.call{value: msg.value}("");

if (deposited == false) revert DepositFailed();

emit Deposit(msg.sender, receiver, msg.value, address(0), payload);
}

// Deposit ERC20 tokens to custody and call an omnichain smart contract
function depositAndCall(address receiver, uint256 amount, address asset, bytes calldata payload) external {
if (amount == 0) revert InsufficientERC20Amount();
IERC20(asset).safeTransferFrom(msg.sender, address(custody), amount);

emit Deposit(msg.sender, receiver, amount, asset, payload);
}

// Call an omnichain smart contract without asset transfer
function call(address receiver, bytes calldata payload) external {
emit Call(msg.sender, receiver, payload);
}

function setCustody(address _custody) external {
custody = _custody;
}

function resetApproval(address token, address to) private returns (bool) {
return IERC20(token).approve(to, 0);
}
}
118 changes: 118 additions & 0 deletions contracts/prototypes/evm/GatewayEVMUpgradeTest.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.7;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";


// NOTE: Purpose of this contract is to test upgrade process, the only difference should be name of Executed event
// The Gateway contract is the endpoint to call smart contracts on external chains
// The contract doesn't hold any funds and should never have active allowances
contract GatewayEVMUpgradeTest is Initializable, OwnableUpgradeable, UUPSUpgradeable {
using SafeERC20 for IERC20;

error ExecutionFailed();
error SendFailed();
error InsufficientETHAmount();
error ZeroAddress();
error ApprovalFailed();

address public custody;
address public tssAddress;

event ExecutedV2(address indexed destination, uint256 value, bytes data);
event ExecutedWithERC20(address indexed token, address indexed to, uint256 amount, bytes data);
event SendERC20(bytes recipient, address indexed asset, uint256 amount);
event Send(bytes recipient, uint256 amount);

/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers();
}

function initialize(address _tssAddress) public initializer {
__Ownable_init();
__UUPSUpgradeable_init();

if (_tssAddress == address(0)) revert ZeroAddress();

tssAddress = _tssAddress;
}

function _authorizeUpgrade(address newImplementation) internal override onlyOwner() {}

function _execute(address destination, bytes calldata data) internal returns (bytes memory) {
(bool success, bytes memory result) = destination.call{value: msg.value}(data);

if (!success) revert ExecutionFailed();

return result;
}

// Called by the TSS
// Execution without ERC20 tokens, it is payable and can be used in the case of WithdrawAndCall for Gas ZRC20
// It can be also used for contract call without asset movement
function execute(address destination, bytes calldata data) external payable returns (bytes memory) {
bytes memory result = _execute(destination, data);

emit ExecutedV2(destination, msg.value, data);

return result;
}

// Called by the ERC20Custody contract
// It call a function using ERC20 transfer
// Since the goal is to allow calling contract not designed for ZetaChain specifically, it uses ERC20 allowance system
// It provides allowance to destination contract and call destination contract. In the end, it remove remaining allowance and transfer remaining tokens back to the custody contract for security purposes
function executeWithERC20(
address token,
address to,
uint256 amount,
bytes calldata data
) external returns (bytes memory) {
// Approve the target contract to spend the tokens
if(!IERC20(token).approve(to, 0)) revert ApprovalFailed();
if(!IERC20(token).approve(to, amount)) revert ApprovalFailed();

// Execute the call on the target contract
bytes memory result = _execute(to, data);

// Reset approval
if(!IERC20(token).approve(to, 0)) revert ApprovalFailed();

// Transfer any remaining tokens back to the custody contract
uint256 remainingBalance = IERC20(token).balanceOf(address(this));
if (remainingBalance > 0) {
IERC20(token).safeTransfer(address(custody), remainingBalance);
}

emit ExecutedWithERC20(token, to, amount, data);

return result;
}

// Transfer specified token amount to ERC20Custody and emits event
function sendERC20(bytes calldata recipient, address token, uint256 amount) external {
IERC20(token).safeTransferFrom(msg.sender, address(custody), amount);

emit SendERC20(recipient, token, amount);
}

// Transfer specified ETH amount to TSS address and emits event
function send(bytes calldata recipient, uint256 amount) external payable {
if (msg.value == 0) revert InsufficientETHAmount();

(bool sent, ) = tssAddress.call{value: msg.value}("");

if (sent == false) revert SendFailed();

emit Send(recipient, msg.value);
}

function setCustody(address _custody) external {
custody = _custody;
}
}
37 changes: 37 additions & 0 deletions contracts/prototypes/evm/Receiver.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.7;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

contract Receiver {
using SafeERC20 for IERC20;

event ReceivedPayable(address sender, uint256 value, string str, uint256 num, bool flag);
event ReceivedNonPayable(address sender, string[] strs, uint256[] nums, bool flag);
event ReceivedERC20(address sender, uint256 amount, address token, address destination);
event ReceivedNoParams(address sender);

// Payable function
function receivePayable(string memory str, uint256 num, bool flag) external payable {
emit ReceivedPayable(msg.sender, msg.value, str, num, flag);
}

// Non-payable function
function receiveNonPayable(string[] memory strs, uint256[] memory nums, bool flag) external {
emit ReceivedNonPayable(msg.sender, strs, nums, flag);
}

// Function using IERC20
function receiveERC20(uint256 amount, address token, address destination) external {
// Transfer tokens from the Gateway contract to the destination address
IERC20(token).safeTransferFrom(msg.sender, destination, amount);

emit ReceivedERC20(msg.sender, amount, token, destination);
}

// Function without parameters
function receiveNoParams() external {
emit ReceivedNoParams(msg.sender);
}
}
12 changes: 12 additions & 0 deletions contracts/prototypes/evm/TestERC20.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.7;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract TestERC20 is ERC20 {
constructor(string memory name, string memory symbol) ERC20(name, symbol) {}

function mint(address to, uint256 amount) external {
_mint(to, amount);
}
}
17 changes: 17 additions & 0 deletions contracts/prototypes/evm/interfaces.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.7;

interface IGatewayEVM {
function executeWithERC20(
address token,
address to,
uint256 amount,
bytes calldata data
) external returns (bytes memory);

function execute(address destination, bytes calldata data) external payable returns (bytes memory);

function sendERC20(bytes calldata recipient, address asset, uint256 amount) external;

function send(bytes calldata recipient, uint256 amount) external payable;
}
Loading
Loading