-
Notifications
You must be signed in to change notification settings - Fork 0
UniversalGateway PC + Vault Inclusion #13
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
Changes from 11 commits
9cf74a5
ed314ea
d7bd5b6
4896323
cfcb6ec
a279822
e110820
5ea292a
31160c1
553eaa5
878f9d4
8b9ea30
2e807f2
635e037
ad6e2ec
d000d9e
9eac0da
76ee21b
c56dded
8adcedc
cf02ecd
9dc59fb
ac53538
0e7d4a4
ab115f3
153a03a
06fb18c
94441ff
cac889d
7040091
ad54ed4
85001a5
8a90ebb
eccaa66
ac38b11
d4698c3
c16741a
7d4f8b1
7e56bee
36c25ae
4420afa
2da7c3c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -98,7 +98,8 @@ contract UniversalGateway is | |
| uint256 public l2SequencerGracePeriodSec; // L2 Sequencer grace period. (e.g., 300 seconds) | ||
| AggregatorV3Interface public l2SequencerFeed; // L2 Sequencer uptime feed & grace period for rollups (if set, enforce sequencer up + grace) | ||
|
|
||
|
|
||
| /// @notice Map to track if a payload has been executed | ||
| mapping(bytes32 => bool) public isExecuted; | ||
|
|
||
| /// @notice Gap for future upgrades. | ||
| uint256[43] private __gap; | ||
|
|
@@ -527,6 +528,51 @@ contract UniversalGateway is | |
| emit WithdrawFunds(revertInstruction.fundRecipient, amount, token); | ||
| } | ||
|
|
||
| // ========================= | ||
| // GATEWAY Payload Execution Paths | ||
| // ========================= | ||
|
|
||
| /// @notice Executes a Universal Transaction on this chain triggered by TSS after validation on Push Chain. | ||
| /// @dev Allows outbound payload execution from Push Chain to external chains. | ||
| /// - The tokens used for payload execution, are to be burnt on Push Chain. | ||
| /// - approval and reset of approval is handled by the gateway. | ||
| /// @param txID unique transaction identifier | ||
| /// @param originCaller original caller/user on source chain | ||
| /// @param token token address (address(0) for native) | ||
| /// @param target target contract address to execute call | ||
| /// @param amount amount of token/native to send along | ||
| /// @param payload calldata to be executed on target | ||
| function executeUniversalTx( | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it only supports arbitrary payload execution, but not the onCall/onExecute feature, which is crucial for any universal app development There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. TBD internally. |
||
| bytes32 txID, | ||
| address originCaller, | ||
| address token, | ||
| address target, | ||
| uint256 amount, | ||
| bytes calldata payload | ||
| ) external payable nonReentrant whenNotPaused onlyRole(TSS_ROLE) { | ||
| if (isExecuted[txID]) revert Errors.PayloadExecuted(); | ||
|
|
||
zaryab2000 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| if (target == address(0) || originCaller == address(0)) revert Errors.InvalidInput(); | ||
| if (amount == 0) revert Errors.InvalidAmount(); | ||
|
|
||
| if (token == address(0)) { // native token | ||
| if (msg.value != amount) revert Errors.InvalidAmount(); | ||
| _executeCall(target, payload, amount); | ||
| } else { // ERC20 token | ||
| if (msg.value != 0) revert Errors.InvalidAmount(); | ||
| if (IERC20(token).balanceOf(address(this)) < amount) revert Errors.InvalidAmount(); | ||
|
|
||
| _resetApproval(token, target); // reset approval to zero | ||
| _safeApprove(token, target, amount); // approve target to spend amount | ||
| _executeCall(target, payload, 0); // execute call with required amount | ||
| _resetApproval(token, target); // reset approval back to zero | ||
| } | ||
|
|
||
| isExecuted[txID] = true; | ||
|
|
||
| emit UniversalTxExecuted(txID, originCaller, target, token, amount, payload); | ||
| } | ||
|
|
||
| // ========================= | ||
| // PUBLIC HELPERS | ||
| // ========================= | ||
|
|
@@ -683,6 +729,46 @@ contract UniversalGateway is | |
| IERC20(token).safeTransfer(recipient, amount); | ||
| } | ||
|
|
||
| /// @dev Safely reset approval to zero before granting any new allowance to target contract. | ||
| function _resetApproval(address token, address spender) internal { | ||
| (bool success, bytes memory returnData) = | ||
| token.call(abi.encodeWithSelector(IERC20.approve.selector, spender, 0)); | ||
| if (!success) { | ||
| // Some non-standard tokens revert on zero-approval; treat as reset-ok to avoid breaking the flow. | ||
| return; | ||
| } | ||
| // If token returns a boolean, ensure it is true; if no return data, assume success (USDT-style). | ||
| if (returnData.length > 0) { | ||
| bool approved = abi.decode(returnData, (bool)); | ||
| if (!approved) revert Errors.InvalidData(); | ||
| } | ||
| } | ||
|
|
||
| /// @dev Safely approve ERC20 token spending to a target contract. | ||
| /// Low-level call must succeed AND (if returns data) decode to true; otherwise revert. | ||
| function _safeApprove(address token, address spender, uint256 amount) internal { | ||
| (bool success, bytes memory returnData) = | ||
| token.call(abi.encodeWithSelector(IERC20.approve.selector, spender, amount)); | ||
| if (!success) { | ||
| revert Errors.InvalidData(); // approval failed | ||
| } | ||
| if (returnData.length > 0) { | ||
| bool approved = abi.decode(returnData, (bool)); | ||
| if (!approved) { | ||
| revert Errors.InvalidData(); // approval failed | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /// @dev Unified helper to execute a low-level call to target | ||
| /// Call can be executed with native value or ERC20 token. | ||
| /// Reverts with Errors.ExecutionFailed() if the call fails (no bubbling). | ||
| function _executeCall(address target, bytes calldata payload, uint256 value) internal returns (bytes memory result) { | ||
| (bool success, bytes memory ret) = target.call{value: value}(payload); | ||
| if (!success) revert Errors.ExecutionFailed(); | ||
| return ret; | ||
| } | ||
|
|
||
| /// @dev Enforce and consume the per-token epoch rate limit. | ||
| /// For a token, if threshold is 0, it is unsupported. | ||
| /// epoch.used is reset to 0 when a new epoch starts (no rollover). | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,210 @@ | ||
| // SPDX-License-Identifier: MIT | ||
| pragma solidity 0.8.26; | ||
|
|
||
| /** | ||
| * @title UniversalGatewayPC | ||
| * @notice Push Chain–side outbound gateway. | ||
| * - Allows users to withdraw PRC20 (wrapped) tokens back to the origin chain. | ||
| * - Allows users to withdraw PRC20 and attach a payload for arbitrary call execution on the origin chain. | ||
| * - This contract does NOT handle deposits or inbound transfers. | ||
| * - This contract does NOT custody user assets; PRC20 are burned at request time. | ||
| * | ||
| * @dev Upgradeable via TransparentUpgradeableProxy. | ||
| * Integrates with UniversalCore to discover the Universal Executor Module (fee sink) | ||
| * and with PRC20 tokens to compute gas fees and burn on withdraw. | ||
| */ | ||
| import { Errors } from "./libraries/Errors.sol"; | ||
| import { IPRC20 } from "./interfaces/IPRC20.sol"; | ||
| import { RevertInstructions } from "./libraries/Types.sol"; | ||
| import { IUniversalCore } from "./interfaces/IUniversalCore.sol"; | ||
| import { IUniversalGatewayPC } from "./interfaces/IUniversalGatewayPC.sol"; | ||
|
|
||
| import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; | ||
| import { PausableUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; | ||
| import { AccessControlUpgradeable } from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; | ||
| import { ReentrancyGuardUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; | ||
|
|
||
| contract UniversalGatewayPC is | ||
| Initializable, | ||
| AccessControlUpgradeable, | ||
| ReentrancyGuardUpgradeable, | ||
| PausableUpgradeable, | ||
| IUniversalGatewayPC | ||
| { | ||
| // ========= Roles ========= | ||
| bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE"); | ||
|
|
||
| // ========= State ========= | ||
| /// @notice UniversalCore on Push Chain (provides gas coin/prices + UEM address). | ||
| address public UNIVERSAL_CORE; | ||
|
|
||
| /// @notice Cached Universal Executor Module as fee vault; derived from UniversalCore. | ||
| /// @dev If UniversalCore updates, call {refreshUniversalExecutor} to recache. | ||
| address public UNIVERSAL_EXECUTOR_MODULE; | ||
|
|
||
| // ========= Storage gap for upgradeability ========= | ||
| uint256[47] private __gap; | ||
zaryab2000 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| // ========= Initializer ========= | ||
| function initialize(address admin, address pauser, address universalCore) external initializer { | ||
| if (admin == address(0) || pauser == address(0) || universalCore == address(0)) revert Errors.ZeroAddress(); | ||
|
|
||
| __AccessControl_init(); | ||
| __ReentrancyGuard_init(); | ||
| __Pausable_init(); | ||
|
|
||
| _grantRole(DEFAULT_ADMIN_ROLE, admin); | ||
| _grantRole(PAUSER_ROLE, pauser); | ||
|
|
||
| UNIVERSAL_CORE = universalCore; | ||
| UNIVERSAL_EXECUTOR_MODULE = IUniversalCore(universalCore).UNIVERSAL_EXECUTOR_MODULE(); | ||
| } | ||
|
|
||
| // ========= Admin ========= | ||
| function setUniversalCore(address universalCore) external onlyRole(DEFAULT_ADMIN_ROLE) { | ||
| if (universalCore == address(0)) revert Errors.ZeroAddress(); | ||
| UNIVERSAL_CORE = universalCore; | ||
| // Refresh UEM from the new core | ||
| UNIVERSAL_EXECUTOR_MODULE = IUniversalCore(universalCore).UNIVERSAL_EXECUTOR_MODULE(); | ||
| } | ||
|
|
||
| function refreshUniversalExecutor() external onlyRole(DEFAULT_ADMIN_ROLE) { | ||
zaryab2000 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| UNIVERSAL_EXECUTOR_MODULE = IUniversalCore(UNIVERSAL_CORE).UNIVERSAL_EXECUTOR_MODULE(); | ||
| } | ||
|
|
||
| function pause() external onlyRole(PAUSER_ROLE) whenNotPaused { | ||
| _pause(); | ||
| } | ||
|
|
||
| function unpause() external onlyRole(PAUSER_ROLE) whenPaused { | ||
| _unpause(); | ||
| } | ||
|
|
||
| // ========= User Flows ========= | ||
|
|
||
| /** | ||
| * @notice Withdraw PRC20 back to origin chain (funds only). | ||
| * @param to Raw destination address on origin chain. | ||
| * @param token PRC20 token address on Push Chain. | ||
| * @param amount Amount to withdraw (burn on Push, unlock at origin). | ||
| * @param gasLimit Gas limit to use for fee quote; if 0, uses token's default GAS_LIMIT(). | ||
| * @param revertInstruction Revert configuration (fundRecipient, revertMsg) for off-chain use. | ||
| */ | ||
| function withdraw( | ||
| bytes calldata to, | ||
| address token, | ||
| uint256 amount, | ||
| uint256 gasLimit, | ||
| RevertInstructions calldata revertInstruction | ||
| ) external whenNotPaused nonReentrant { | ||
| _validateCommon(to, token, amount, revertInstruction); | ||
|
|
||
| // Compute fees + collect from caller into the UEM fee sink | ||
| (address gasToken, uint256 gasFee, uint256 gasLimitUsed, uint256 protocolFee) = | ||
| _calculateGasFeesWithLimit(token, gasLimit); | ||
| _moveFees(msg.sender, gasToken, gasFee); | ||
|
|
||
| // Move PRC20 to this contract and burn on Push Chain | ||
| _burnPRC20(msg.sender, token, amount); | ||
|
|
||
| // Emit | ||
| string memory chainId = IPRC20(token).SOURCE_CHAIN_ID(); | ||
| emit UniversalTxWithdraw( | ||
| msg.sender, chainId, token, to, amount, gasToken, gasFee, gasLimitUsed, bytes(""), protocolFee, revertInstruction | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * @notice Withdraw PRC20 and attach an arbitrary payload to be executed on the origin chain. | ||
| * @param target Raw destination (contract) address on origin chain. | ||
| * @param token PRC20 token address on Push Chain. | ||
| * @param amount Amount to withdraw (burn on Push, unlock at origin). | ||
| * @param payload ABI-encoded calldata to execute on the origin chain. | ||
| * @param gasLimit Gas limit to use for fee quote; if 0, uses token's default GAS_LIMIT(). | ||
| * @param revertInstruction Revert configuration (fundRecipient, revertMsg) for off-chain use. | ||
| */ | ||
| function withdrawAndExecute( | ||
| bytes calldata target, | ||
| address token, | ||
| uint256 amount, | ||
| bytes calldata payload, | ||
| uint256 gasLimit, | ||
| RevertInstructions calldata revertInstruction | ||
| ) external whenNotPaused nonReentrant { | ||
| _validateCommon(target, token, amount, revertInstruction); | ||
|
|
||
| // Compute fees + collect from caller into the UEM fee sink | ||
| (address gasToken, uint256 gasFee, uint256 gasLimitUsed, uint256 protocolFee) = | ||
| _calculateGasFeesWithLimit(token, gasLimit); | ||
| _moveFees(msg.sender, gasToken, gasFee); | ||
|
|
||
| // Move PRC20 to this contract and burn on Push Chain | ||
| _burnPRC20(msg.sender, token, amount); | ||
|
|
||
| // Emit | ||
| string memory chainId = IPRC20(token).SOURCE_CHAIN_ID(); | ||
| emit UniversalTxWithdraw( | ||
| msg.sender, chainId, token, target, amount, gasToken, gasFee, gasLimitUsed, payload, protocolFee, revertInstruction | ||
| ); | ||
| } | ||
|
|
||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. execute() fn is missing |
||
| // ========= Helpers ========= | ||
|
|
||
| function _validateCommon( | ||
| bytes calldata rawTarget, | ||
| address token, | ||
| uint256 amount, | ||
| RevertInstructions calldata revertInstruction | ||
| ) internal pure { | ||
| if (rawTarget.length == 0) revert Errors.InvalidInput(); | ||
| if (token == address(0)) revert Errors.ZeroAddress(); | ||
| if (amount == 0) revert Errors.InvalidAmount(); | ||
| if (revertInstruction.fundRecipient == address(0)) revert Errors.InvalidRecipient(); | ||
| } | ||
|
|
||
| /** | ||
| * @dev Use the PRC20's withdrawGasFeeWithGasLimit to compute fee (gas coin + amount). | ||
| * If gasLimit = 0, pull the default token.GAS_LIMIT(). | ||
| * @return gasToken PRC20 address to be used for fee payment. | ||
| * @return gasFee Amount of gasToken to collect from the user (includes protocol fee). | ||
| * @return gasLimitUsed Gas limit actually used for the quote. | ||
| * @return protocolFee The flat protocol fee component (as exposed by PRC20). | ||
| */ | ||
| function _calculateGasFeesWithLimit(address token, uint256 gasLimit) | ||
| internal | ||
| view | ||
| returns (address gasToken, uint256 gasFee, uint256 gasLimitUsed, uint256 protocolFee) | ||
| { | ||
| if (gasLimit == 0) { | ||
| gasLimitUsed = IPRC20(token).GAS_LIMIT(); | ||
| } else { | ||
| gasLimitUsed = gasLimit; | ||
| } | ||
|
|
||
| (gasToken, gasFee) = IPRC20(token).withdrawGasFeeWithGasLimit(gasLimitUsed); | ||
| if (gasToken == address(0) || gasFee == 0) revert Errors.InvalidData(); | ||
|
|
||
| protocolFee = IPRC20(token).PC_PROTOCOL_FEE(); | ||
| } | ||
|
|
||
| /** | ||
| * @dev Pull fee from user into the Universal Executor Module. | ||
| * Caller must have approved `gasToken` for at least `gasFee`. | ||
| */ | ||
| function _moveFees(address from, address gasToken, uint256 gasFee) internal { | ||
| address _ueModule = UNIVERSAL_EXECUTOR_MODULE; | ||
| if (_ueModule == address(0)) revert Errors.ZeroAddress(); | ||
|
|
||
zaryab2000 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| bool ok = IPRC20(gasToken).transferFrom(from, _ueModule, gasFee); | ||
| if (!ok) revert Errors.GasFeeTransferFailed(gasToken, from, gasFee); | ||
| } | ||
|
|
||
| function _burnPRC20(address from, address token, uint256 amount) internal { | ||
| // Pull PRC20 into this gateway first | ||
| IPRC20(token).transferFrom(from, address(this), amount); | ||
|
|
||
| // Then burn from this contract's balance | ||
| bool ok = IPRC20(token).burn(amount); | ||
Zartaj0 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| if (!ok) revert Errors.TokenBurnFailed(token, amount); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| // SPDX-License-Identifier: MIT | ||
| pragma solidity 0.8.26; | ||
|
|
||
| interface IPRC20 { | ||
|
|
||
| function SOURCE_CHAIN_ID() external view returns (string memory); | ||
| function GAS_LIMIT() external view returns (uint256); | ||
| function PC_PROTOCOL_FEE() external view returns (uint256); | ||
|
|
||
| // ERC20 subset | ||
| function decimals() external view returns (uint8); | ||
| function transferFrom(address from, address to, uint256 value) external returns (bool); | ||
| function approve(address spender, uint256 amount) external returns (bool); | ||
|
|
||
| // PRC20 extensions | ||
| function withdrawGasFee() external view returns (address gasToken, uint256 gasFee); | ||
| function withdrawGasFeeWithGasLimit(uint256 gasLimit) external view returns (address gasToken, uint256 gasFee); | ||
| function burn(uint256 amount) external returns (bool); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| // SPDX-License-Identifier: MIT | ||
| pragma solidity 0.8.26; | ||
|
|
||
|
|
||
| interface IUniversalCore { | ||
| function UNIVERSAL_EXECUTOR_MODULE() external view returns (address); | ||
| function gasTokenPRC20ByChainId(string calldata chainId) external view returns (address); | ||
| function gasPriceByChainId(string calldata chainId) external view returns (uint256); | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.