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:
// 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.