-
Notifications
You must be signed in to change notification settings - Fork 51
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(contracts): allow xapps to swap eth for gas on omni (#1756)
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
1 parent
5b1775d
commit 4de2074
Showing
4 changed files
with
415 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
Oops, something went wrong.