Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
9cf74a5
executePayload-gateway added
zaryab2000 Oct 15, 2025
ed314ea
fix issue
zaryab2000 Oct 16, 2025
d7bd5b6
added UniversalGatewayPC
zaryab2000 Oct 16, 2025
4896323
test for gateway-execute
zaryab2000 Oct 16, 2025
cfcb6ec
removed redundancy
zaryab2000 Oct 20, 2025
a279822
fmt
zaryab2000 Oct 20, 2025
e110820
executePayload tests
zaryab2000 Oct 20, 2025
5ea292a
fmt
zaryab2000 Oct 20, 2025
31160c1
missing param added
zaryab2000 Oct 20, 2025
553eaa5
fixed bug
zaryab2000 Oct 20, 2025
878f9d4
test-gatewayPC
zaryab2000 Oct 20, 2025
8b9ea30
natspec fixes
zaryab2000 Oct 20, 2025
2e807f2
revertMsg to revertContext
zaryab2000 Oct 20, 2025
635e037
revertMsg to revertContext
zaryab2000 Oct 20, 2025
ad6e2ec
coverage
zaryab2000 Oct 20, 2025
d000d9e
added isSupportedTokens helper
zaryab2000 Oct 21, 2025
9eac0da
included Vault contract
zaryab2000 Oct 21, 2025
76ee21b
IVault + mocks
zaryab2000 Oct 21, 2025
c56dded
vault-changes
zaryab2000 Oct 21, 2025
8adcedc
fixed vault-bugs
zaryab2000 Oct 21, 2025
cf02ecd
uniGateway changes w.r.t vault.sol
zaryab2000 Oct 21, 2025
9dc59fb
test fixes
zaryab2000 Oct 21, 2025
ac53538
fixed broken tests
zaryab2000 Oct 22, 2025
0e7d4a4
updated mocks PRC+UC
zaryab2000 Oct 22, 2025
ab115f3
updated failing tests
zaryab2000 Oct 22, 2025
153a03a
updated interfaces
zaryab2000 Oct 22, 2025
06fb18c
fixed bugs
zaryab2000 Oct 22, 2025
94441ff
gatewayPC update w.r.t. UC
zaryab2000 Oct 22, 2025
cac889d
testnet gateway updated
zaryab2000 Oct 22, 2025
7040091
minimized VaultPC
zaryab2000 Oct 28, 2025
ad54ed4
whenNotPaused removed + txId added in vault.Withdraw()
zaryab2000 Oct 28, 2025
85001a5
removed _gap and refreshUniversalCore()
zaryab2000 Oct 28, 2025
8a90ebb
follow CEI pattern
zaryab2000 Oct 28, 2025
eccaa66
isSupportedToken in UC + _enforceSupportedToken VaultPC
zaryab2000 Oct 30, 2025
ac38b11
vault+gateway revert tx changes
zaryab2000 Oct 30, 2025
d4698c3
vault+gateway revert tx changes
zaryab2000 Oct 30, 2025
c16741a
added IVaultPC interface
zaryab2000 Oct 30, 2025
7d4f8b1
vaultPC for fees in gatewayPC
zaryab2000 Oct 30, 2025
7e56bee
interfaces + natspec fixes
zaryab2000 Oct 30, 2025
36c25ae
vault test coverage bump-up
zaryab2000 Oct 30, 2025
4420afa
vaulPC tests + coverage bump
zaryab2000 Oct 30, 2025
2da7c3c
removed pauser-role
zaryab2000 Oct 30, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 0 additions & 10 deletions contracts/evm-gateway/.env.sample

This file was deleted.

88 changes: 87 additions & 1 deletion contracts/evm-gateway/src/UniversalGateway.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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(
Copy link
Member

Choose a reason for hiding this comment

The 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

Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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();

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
// =========================
Expand Down Expand Up @@ -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).
Expand Down
210 changes: 210 additions & 0 deletions contracts/evm-gateway/src/UniversalGatewayPC.sol
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;

// ========= 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) {
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
);
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

execute() fn is missing
If a user only wants to execute an outbound payload without withdrawal, then execute fn needs to be there

// ========= 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();

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);
if (!ok) revert Errors.TokenBurnFailed(token, amount);
}
}
19 changes: 19 additions & 0 deletions contracts/evm-gateway/src/interfaces/IPRC20.sol
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);
}
9 changes: 9 additions & 0 deletions contracts/evm-gateway/src/interfaces/IUniversalCore.sol
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);
}
10 changes: 10 additions & 0 deletions contracts/evm-gateway/src/interfaces/IUniversalGateway.sol
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,16 @@ interface IUniversalGateway {
TX_TYPE txType,
bytes signatureData
);
/// @notice Universal tx execution event. Emits for outbound transactions from Push Chain to external chains
event UniversalTxExecuted(
bytes32 indexed txID,
address indexed originCaller,
address indexed target,
address token,
uint256 amount,
bytes data
);

/// @notice Withdraw funds event
event WithdrawFunds(address indexed recipient, uint256 amount, address tokenAddress);
/// @notice Caps updated event
Expand Down
Loading