Skip to main content
Developers
Omnichain Contracts
Tutorials
Curve on zEVM

Curve on zEVM

Overview

Assuming you have familiarized yourself with ZRC-20 Tokens and zEVM, this example walks through how you'd create an omnichain Curve pool! This means you can leverage the existing Curve contracts and orchestrate external, native assets as if they were all on one chain.

Deploy Curve on ZetaChain

Since zEVM is fully EVM compatible, you can download the Curve repo as it is and deploy it on zEVM, simply pointing the RPC to zEVM RPC. You can find all the ZetaChain RPC information here.

Deploy a tri-token pool of ZRC-20 tokens

Let's say we already deployed a tri-token pool (if you don't know how to deploy it take a look to official script, does all the work for you out of the box deployment script), using the address of three ZRC-2020 tokens. You can find the ZetaChain addresses of ZRC-20 tokens supported right now on the ZetaChain testnet using this endpoint.

Implement a cross-chain stableswap

Now that you have Curve and the pool you want deployed, swapping would look just like this:

packages/zevm-app-contracts/contracts/zeta-swap/ZetaCurveSwapDemo.sol
// SPDX-License-Identifier: MIT
pragma solidity 0.8.7;

import "@zetachain/protocol-contracts/contracts/zevm/interfaces/zContract.sol";
import "@zetachain/protocol-contracts/contracts/zevm/interfaces/IZRC20.sol";

interface ICRV3 {
function exchange(uint256 i, uint256 j, uint256 dx, uint256 min_dy, bool use_eth) external returns (uint256);
}

interface ZetaCurveSwapErrors {
error WrongGasContract();

error NotEnoughToPayGasFee();

error InvalidAddress();
}

contract ZetaCurveSwapDemo is zContract, ZetaCurveSwapErrors {
address public crv3pool; // gETH/tBNB/tMATIC pool
address[3] public crvZRC20s;

constructor(address crv3pool_, address[3] memory ZRC20s_) {
if (crv3pool_ == address(0) || ZRC20s_[0] == address(0) || ZRC20s_[1] == address(0) || ZRC20s_[2] == address(0))
revert InvalidAddress();
crv3pool = crv3pool_;
crvZRC20s = ZRC20s_;
}

function encode(address zrc20, address recipient, uint256 minAmountOut) public pure returns (bytes memory) {
return abi.encode(zrc20, recipient, minAmountOut);
}

function addr2idx(address zrc20) public view returns (uint256) {
for (uint256 i = 0; i < 3; i++) {
if (crvZRC20s[i] == zrc20) {
return i;
}
}
return 18;
}

function _doWithdrawal(address targetZRC20, uint256 amount, bytes32 receipient) private {
(address gasZRC20, uint256 gasFee) = IZRC20(targetZRC20).withdrawGasFee();

if (gasZRC20 != targetZRC20) revert WrongGasContract();
if (gasFee >= amount) revert NotEnoughToPayGasFee();

IZRC20(targetZRC20).approve(targetZRC20, gasFee);
IZRC20(targetZRC20).withdraw(abi.encodePacked(receipient), amount - gasFee);
}

function onCrossChainCall(
zContext calldata context,
address zrc20,
uint256 amount,
bytes calldata message
) external override {
(address targetZRC20, bytes32 receipient, ) = abi.decode(message, (address, bytes32, uint256));

address[] memory path = new address[](2);
path[0] = zrc20;
path[1] = targetZRC20;
IZRC20(zrc20).approve(address(crv3pool), amount);

uint256 i = addr2idx(zrc20);
uint256 j = addr2idx(targetZRC20);
require(i >= 0 && i < 3 && j >= 0 && j < 3 && i != j, "i,j error");

uint256 outAmount = ICRV3(crv3pool).exchange(i, j, amount, 0, false);

_doWithdrawal(targetZRC20, outAmount, receipient);
}
}

In this example crvZRC20s is an array of three ZRC20 tokens, for example gETH, tBNB and tMATIC. And crv3pool is the address of the pool you deployed with Curve's code.

Easy right? In order to swap, you just need to write onCrossChainCall. This function simply extracts params from the message, calls the Curve pool's exchange, and then withdraws to the designated destination. All swap/pool logic remains in the core Curve contract deployment. Users can interact by depositing and calling this zEVM contract from an external chain. You can see how you'd call this for a user programmatically here.