Skip to content

Commit

Permalink
feat(contracts): allow xapps to swap eth for gas on omni (#1756)
Browse files Browse the repository at this point in the history
Allow xapps to swap eth for gas on omni.

Components:
- OmniXFund: contract that pays out OMNI on Omni
- OmniGasPump: contract on each rollups that accepts ETH payments, and
directs OmniFund to pay out
- XGasPump: abstract contract that can be used to extend you xapp with
helpers to fund accounts on Omni

issue: #1686
  • Loading branch information
kevinhalliday authored Aug 28, 2024
1 parent 5b1775d commit 4de2074
Show file tree
Hide file tree
Showing 4 changed files with 415 additions and 0 deletions.
44 changes: 44 additions & 0 deletions contracts/core/src/examples/Funder.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// SPDX-License-Identifier: Unlicense
pragma solidity 0.8.24;

import { XApp } from "src/pkg/XApp.sol";
import { XGasPump } from "src/pkg/XGasPump.sol";
import { ConfLevel } from "src/libraries/ConfLevel.sol";

/**
* @title Funder
* @notice Example contract that shows how to use XGasPump
*/
contract Funder is XApp, XGasPump {
address public thingDoer;

constructor(address portal, address pump) XApp(portal, ConfLevel.Latest) XGasPump(pump) { }

/**
* @notice Simple external method to let msg.sender swap msg.value ETH for OMNI, on Omni
*/
function getOMNI() external payable {
fillUp(msg.sender, msg.value);
}

/**
* @notice Example of doing an xcall, and using excess msg.value to fund the caller on Omni,
* if they paid enough
*/
function doThingAndMaybeGetOMNI() external payable {
uint256 fee = xcall({
destChainId: omniChainId(),
to: thingDoer,
data: abi.encodeWithSignature("doThing()"),
gasLimit: 100_000
});

require(msg.value >= fee, "Funder: insufficient fee");

if (msg.value > fee) fillUpOrRefund(msg.sender, msg.value - fee);
}

function doThingFee() external view returns (uint256) {
return feeFor({ destChainId: omniChainId(), data: abi.encodeWithSignature("doThing()"), gasLimit: 100_000 });
}
}
60 changes: 60 additions & 0 deletions contracts/core/src/pkg/XGasPump.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity ^0.8.12;

import { OmniGasPump } from "../token/OmniGasPump.sol";

/**
* @title XGasPump
* @notice Abstract contract that makes it easy to swap ETH for OMNI on Omni.
*/
abstract contract XGasPump {
event Refunded(address indexed recipient, uint256 amtETH, string reason);
event FundedOMNI(address indexed recipient, uint256 ethPaid, uint256 omniReceived);

OmniGasPump public immutable omniGasPump;

constructor(address exchange) {
omniGasPump = OmniGasPump(exchange);
}

/**
* @notice Swap `amtETH` ETH for OMNI on Omni, funding `recipient`.
* Reverts if `amtETH` does not cover xcall fee, or is > max allowed swap.
*/
function fillUp(address recipient, uint256 amtETH) internal {
_fillUp(recipient, amtETH);
}

/**
* @notice Fund `recipient` with `amtETH` worth of OMNI on Omni.
* If `amtETH` is not swappable, refund to `recipient`.
*/
function fillUpOrRefund(address recipient, uint256 amtETH) internal {
_fillUpOrRefund(recipient, recipient, amtETH);
}

/**
* @notice Fund `recipient` with `amtETH` worth of OMNI on Omni.
* If `amtETH` is not swappable, refund to `refundTo`.
*/
function fillUpOrRefund(address refundTo, address recipient, uint256 amtETH) internal {
_fillUpOrRefund(refundTo, recipient, amtETH);
}

function _fillUpOrRefund(address refundTo, address recipient, uint256 amtETH) internal {
(, bool succes, string memory reason) = omniGasPump.dryFillUp(amtETH);

if (!succes) {
emit Refunded(refundTo, amtETH, reason);
payable(refundTo).transfer(amtETH);
return;
}

_fillUp(recipient, amtETH);
}

function _fillUp(address recipient, uint256 amtETH) private {
uint256 amtOMNI = omniGasPump.fillUp{ value: amtETH }(recipient);
emit FundedOMNI(recipient, amtETH, amtOMNI);
}
}
225 changes: 225 additions & 0 deletions contracts/core/src/token/OmniGasPump.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity 0.8.24;

import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import { PausableUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol";
import { XAppUpgradeable } from "src/pkg/XAppUpgradeable.sol";
import { FeeOracleV1 } from "src/xchain/FeeOracleV1.sol";
import { ConfLevel } from "src/libraries/ConfLevel.sol";
import { OmniGasStation } from "./OmniGasStation.sol";

/**
* @title OmniGasPump
* @notice A unidirectional cross-chain gas exchange, allowing users to swap native ETH for native OMNI.
*/
contract OmniGasPump is XAppUpgradeable, OwnableUpgradeable, PausableUpgradeable {
/// @notice Emitted when the max swap is set
event MaxSwapSet(uint256 max);

/// @notice Emitted when the gas station is set
event GasStationSet(address station);

/// @notice Emitted when the toll is set
event TollSet(uint256 pct);

/**
* @notice Emitted on a fillUp
* @param recipient Address on Omni to send OMNI to
* @param owed Total amount owed to `recipient` (sum of historical fillUps()), denominated in OMNI.
* @param amtETH Amount of ETH paid
* @param fee Xcall fee, denominated in ETH
* @param toll Toll taken by this contract, denominated in ETH
* @param amtOMNI Amount of OMNI swapped for
*/
event FilledUp(address indexed recipient, uint256 owed, uint256 amtETH, uint256 fee, uint256 toll, uint256 amtOMNI);

/// @notice Gas limit passed to OmniGasStation.settleUp xcall
uint64 internal constant SETTLE_GAS = 100_000;

/// @notice Denominator for toll percentage calculations
uint256 internal constant TOLL_DENOM = 1000;

/// @notice Address of OmniGasStation on Omni
address public gasStation;

/// @notice Max amt (in native token) that can be swapped in a single tx
uint256 public maxSwap;

/// @notice Percentage toll taken by this contract for each swap, to disencentivize spamming
uint256 public toll;

/// @notice Map recipient to total owed (sum of historical fillUps()), denominated in OMNI.
mapping(address => uint256) public owed;

constructor() {
_disableInitializers();
}

struct InitParams {
address gasStation;
address portal;
address owner;
uint256 maxSwap;
uint256 toll;
}

function initialize(InitParams calldata p) external initializer {
_setGasStation(p.gasStation);
_setMaxSwap(p.maxSwap);
_setToll(p.toll);
__XApp_init(p.portal, ConfLevel.Latest);
__Ownable_init(p.owner);
}

/**
* @notice Swaps msg.value ETH for OMNI and sends it to `recipient` on Omni.
*
* Takes an xcall fee and a pct cut. Cut taken to disencentivize spamming.
* Returns the amount of OMNI swapped for.
*
* To retry (if OmniGasStation transfer fails), call swap() again with the
* same `recipient`, and msg.value == swapFee().
*
* @param recipient Address on Omni to send OMNI to
*/
function fillUp(address recipient) public payable whenNotPaused returns (uint256) {
// take xcall fee
uint256 f = xfee();
require(msg.value >= f, "OmniGasPump: insufficient fee");
uint256 amtETH = msg.value - f;

// check max
require(amtETH <= maxSwap, "OmniGasPump: over max");

// take toll
uint256 t = amtETH * toll / TOLL_DENOM;
amtETH -= t;

uint256 amtOMNI = _toOmni(amtETH);

// update owed
owed[recipient] += amtOMNI;

// settle up with the gas station
xcall({
destChainId: omniChainId(),
to: gasStation,
conf: ConfLevel.Latest,
data: abi.encodeCall(OmniGasStation.settleUp, (recipient, owed[recipient])),
gasLimit: SETTLE_GAS
});

emit FilledUp(recipient, owed[recipient], msg.value, f, t, amtOMNI);

return amtOMNI;
}

/**
* @notice Simulate a fillUp()
* Returns the amount of OMNI that `amtETH` msg.value would buy, whether
* or not it would succeed, and the revert reason, if any.
*/
function dryFillUp(uint256 amtETH) public view returns (uint256, bool, string memory) {
// take xcall fee
uint256 f = xfee();
if (amtETH < f) return (0, false, "insufficient fee");
amtETH = amtETH - f;

// check max
if (amtETH > maxSwap) return (0, false, "over max");

// take toll
amtETH -= amtETH * toll / TOLL_DENOM;

return (_toOmni(amtETH), true, "");
}

/// @notice Returns the xcall fee required for fillUp(). Does not include `pctCut`.
function xfee() public view returns (uint256) {
// Use max addrs & amount to use no zero byte calldata to ensure max fee
address recipient = address(type(uint160).max);
uint256 amt = type(uint256).max;

return feeFor({
destChainId: omniChainId(),
data: abi.encodeCall(OmniGasStation.settleUp, (recipient, amt)),
gasLimit: SETTLE_GAS
});
}

/// @notice Returns the amount of ETH needed to swap for `amtOMNI`
function quote(uint256 amtOMNI) public view returns (uint256) {
uint256 amtETH = _toEth(amtOMNI);

// "undo" toll
amtETH += (amtETH * TOLL_DENOM / (TOLL_DENOM - toll));

// "undo" xcall fee
return amtETH + xfee();
}

/// @notice Converts `amtOMNI` to ETH, using the current conversion rate
function _toEth(uint256 amtOMNI) internal view returns (uint256) {
FeeOracleV1 o = FeeOracleV1(omni.feeOracle());
return amtOMNI * o.CONVERSION_RATE_DENOM() / o.toNativeRate(omniChainId());
}

/// @notice Converts `amtETH` to OMNI, using the current conversion rate
function _toOmni(uint256 amtETH) internal view returns (uint256) {
FeeOracleV1 o = FeeOracleV1(omni.feeOracle());
return amtETH * o.toNativeRate(omniChainId()) / o.CONVERSION_RATE_DENOM();
}

//////////////////////////////////////////////////////////////////////////////
// Admin //
//////////////////////////////////////////////////////////////////////////////

/// @notice Pause fill ups
function pause() external onlyOwner {
_pause();
}

/// @notice Unpause fill ups
function unpause() external onlyOwner {
_unpause();
}

/// @notice Withdraw collected ETH to `to`
function withdraw(address to) external onlyOwner {
(bool success,) = to.call{ value: address(this).balance }("");
require(success, "OmniGasPump: withdraw failed");
}

/// @notice Set the max swap, denominated in ETh
function setMaxSwap(uint256 max) external onlyOwner {
_setMaxSwap(max);
}

/// @notice Set the address of the OmniGasStation, on Omni
function setOmniGasStation(address station) external onlyOwner {
_setGasStation(station);
}

/// @notice Set the toll (as a percentage over PCT_DENOM)
function setToll(uint256 pct) external onlyOwner {
_setToll(pct);
}

function _setToll(uint256 pct) internal {
require(pct < TOLL_DENOM, "OmniGasPump: pct too high");
toll = pct;
emit TollSet(toll);
}

function _setMaxSwap(uint256 max) internal {
require(max > 0, "OmniGasPump: zero max");
maxSwap = max;
emit MaxSwapSet(max);
}

function _setGasStation(address station) internal {
require(station != address(0), "OmniGasPump: zero address");
gasStation = station;
emit GasStationSet(station);
}
}
Loading

0 comments on commit 4de2074

Please sign in to comment.