Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 2 additions & 3 deletions app/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,10 +148,9 @@ Push Chain ships exactly one custom precompile:

| Address | Name | Purpose |
|---|---|---|
| `0x00000000000000000000000000000000000000ca` | `usigverifier` (legacy) | Ed25519 signature verification (Solana signatures over `bytes32` digests) |
| `0xEC00000000000000000000000000000000000001` | `usigverifier` (v2) | Same implementation, registered at the reserved Push range |
| `0xEC00000000000000000000000000000000000001` | `usigverifier` | Ed25519 signature verification (Solana signatures over `bytes32` digests), registered at the reserved Push range |

Both addresses are registered simultaneously for backward compatibility with deployed contracts that have the legacy address hardcoded. Gas cost: `4000` per `verifyEd25519` call. See [`precompiles/usigverifier/README.md`](../precompiles/usigverifier/README.md).
Gas cost: `4000` per `verifyEd25519` call. See [`precompiles/usigverifier/README.md`](../precompiles/usigverifier/README.md).

The baseline EVM precompiles (`bech32`, `p256`, `staking`, `distribution`, `ics20`, `bank`, `gov`, `slashing`, `evidence`) are wired in via `app/precompiles.go:NewAvailableStaticPrecompiles`.

Expand Down
15 changes: 5 additions & 10 deletions precompiles/usigverifier/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,14 @@ The only EVM precompile Push Chain ships on top of the cosmos-evm baseline. Veri

| Address | Why it exists |
|---|---|
| `0x00000000000000000000000000000000000000ca` | Original "legacy" address. Hardcoded into contracts deployed before the address-range cleanup. |
| `0xEC00000000000000000000000000000000000001` | New address in the reserved Push precompile range (`0xEC...`). |
| `0xEC00000000000000000000000000000000000001` | Reserved Push precompile range (`0xEC...`). |

Both addresses are registered simultaneously and point at the **same** implementation. Backward compatibility for previously-deployed contracts is the only reason the legacy address still exists. New code should target `0xEC00000000000000000000000000000000000001`.
Registered at `0xEC00000000000000000000000000000000000001`.

Wired into `app/app.go:781-795`:
Wired into `app/app.go`:

```go
usigverifierPrecompile, _ := usigverifierprecompile.NewPrecompile()
usigverifierPrecompileV2, _ := usigverifierprecompile.NewPrecompileV2()
corePrecompiles[usigverifierPrecompile.Address()] = usigverifierPrecompile
usigverifierPrecompileV2, _ := usigverifierprecompile.NewPrecompile()
corePrecompiles[usigverifierPrecompileV2.Address()] = usigverifierPrecompileV2
```

Expand All @@ -26,7 +23,6 @@ corePrecompiles[usigverifierPrecompileV2.Address()] = usigverifierPrecompileV2
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.18;

address constant USigVerifier_PRECOMPILE_ADDRESS = 0x00000000000000000000000000000000000000ca;
address constant USigVerifier_PRECOMPILE_ADDRESS_V2 = 0xEC00000000000000000000000000000000000001;

interface IUSigVerifier {
Expand Down Expand Up @@ -104,8 +100,7 @@ The Go binary embeds `abi.json` via `//go:embed`, so a fresh `make build` will p
```bash
# Make sure the precompile is enabled in the EVM params:
# app_state["evm"]["params"]["active_static_precompiles"] must include
# 0x00000000000000000000000000000000000000ca and/or 0xEC00000000000000000000000000000000000001
# (test_node.sh installs the legacy address by default).
# 0xEC00000000000000000000000000000000000001

cast call 0xEC00000000000000000000000000000000000001 \
"verifyEd25519(bytes,bytes32,bytes)" \
Expand Down
10 changes: 2 additions & 8 deletions precompiles/usigverifier/USigVerifier.sol
Original file line number Diff line number Diff line change
@@ -1,16 +1,10 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.18;

/// @dev The USigVerifier contract's address (legacy).
address constant USigVerifier_PRECOMPILE_ADDRESS = 0x00000000000000000000000000000000000000ca;

/// @dev The USigVerifier contract's new address in the reserved Push precompile range.
/// @dev The USigVerifier precompile address (reserved Push precompile range).
address constant USigVerifier_PRECOMPILE_ADDRESS_V2 = 0xEC00000000000000000000000000000000000001;

/// @dev The IUSigVerifier contract's instance (legacy address).
IUSigVerifier constant USigVerifier_CONTRACT = IUSigVerifier(USigVerifier_PRECOMPILE_ADDRESS);

/// @dev The IUSigVerifier contract's instance (new address).
/// @dev The IUSigVerifier contract instance.
IUSigVerifier constant USigVerifier_CONTRACT_V2 = IUSigVerifier(USigVerifier_PRECOMPILE_ADDRESS_V2);

/// @dev The IUSigVerifier contract's interface.
Expand Down
2 changes: 1 addition & 1 deletion test/utils/bytecode.go

Large diffs are not rendered by default.

43 changes: 0 additions & 43 deletions x/uexecutor/keeper/evm.go
Original file line number Diff line number Diff line change
Expand Up @@ -299,49 +299,6 @@ func (k Keeper) CallPRC20Deposit(
)
}

// Calls UniversalCore Contract to set gas price
func (k Keeper) CallUniversalCoreSetGasPrice(
ctx sdk.Context,
chainID string,
price *big.Int,
) (*evmtypes.MsgEthereumTxResponse, error) {
handlerAddr := common.HexToAddress(uregistrytypes.SYSTEM_CONTRACTS["UNIVERSAL_CORE"].Address)

abi, err := types.ParseUniversalCoreABI()
if err != nil {
return nil, errors.Wrap(err, "failed to parse Handler Contract ABI")
}

ueModuleAccAddress, _ := k.GetUeModuleAddress(ctx)

// Before sending an EVM tx from module
nonce, err := k.GetModuleAccountNonce(ctx)
if err != nil {
return nil, err
}

// increment first (safe for internal modules)
if _, err := k.IncrementModuleAccountNonce(ctx); err != nil {
return nil, err
}

return k.evmKeeper.DerivedEVMCall(
ctx,
abi,
ueModuleAccAddress, // who is sending the transaction
handlerAddr, // destination: Handler contract
big.NewInt(0),
nil,
true, // commit = true (real tx, not simulation)
false, // gasless = false (@dev: we need gas to be emitted in the tx receipt)
true, // module sender = true
&nonce, // manual nonce of module
"setGasPrice",
chainID,
price,
)
}

// Calls UniversalCore Contract to set chain metadata (gas price + chain height).
// The contract uses block.timestamp for the observed-at value.
func (k Keeper) CallUniversalCoreSetChainMeta(
Expand Down
14 changes: 6 additions & 8 deletions x/uexecutor/keeper/gas_fee.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,12 @@ func (k Keeper) GetOutboundTxGasAndFees(ctx sdk.Context, prc20 common.Address, g
gasFee := results[1].(*big.Int)
// protocolFee := results[2].(*big.Int) — not needed for outbound fields
gasPrice := results[3].(*big.Int)

// Derive gasLimit from gasFee / gasPrice
var gasLimit *big.Int
if gasPrice.Sign() > 0 {
gasLimit = new(big.Int).Div(gasFee, gasPrice)
} else {
gasLimit = big.NewInt(0)
}
// chainNamespace := results[4].(string) — not needed for outbound fields
// gasLimitUsed (results[5]) is the exact gas limit the contract resolved
// (caller-supplied or per-chain baseGasLimitByChainNamespace fallback).
// Reading it directly avoids the gasFee/gasPrice round-trip and keeps us
// in lock-step with the contract's own resolution.
gasLimit := results[5].(*big.Int)

return &GasFeeInfo{
GasToken: gasToken,
Expand Down
82 changes: 82 additions & 0 deletions x/uexecutor/keeper/gas_fee_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package keeper_test

import (
"math/big"
"testing"

"github.com/ethereum/go-ethereum/common"
"github.com/pushchain/push-chain-node/x/uexecutor/types"
"github.com/stretchr/testify/require"
)

// TestUniversalCoreABI_GetOutboundTxGasAndFees_Has6Outputs locks in the new
// post-audit schema (the contract added gasLimitUsed as a 6th output).
// Catches accidental ABI reverts and proves Pack/Unpack round-trips.
func TestUniversalCoreABI_GetOutboundTxGasAndFees_Has6Outputs(t *testing.T) {
abi, err := types.ParseUniversalCoreABI()
require.NoError(t, err)

method, ok := abi.Methods["getOutboundTxGasAndFees"]
require.True(t, ok, "getOutboundTxGasAndFees missing from ABI")
require.Len(t, method.Outputs, 6, "expected 6 outputs (post-audit schema added gasLimitUsed)")

// Output names must match the contract field names so future readers can
// map results[i] back to the contract source unambiguously.
wantNames := []string{"gasToken", "gasFee", "protocolFee", "gasPrice", "chainNamespace", "gasLimitUsed"}
for i, want := range wantNames {
require.Equal(t, want, method.Outputs[i].Name, "output[%d] name mismatch", i)
}

// Round-trip: pack a fake response, unpack it, get the same values back.
// This is the contract that GetOutboundTxGasAndFees in keeper/gas_fee.go
// relies on (results[0]=gasToken, results[1]=gasFee, results[3]=gasPrice,
// results[5]=gasLimit).
wantGasToken := common.HexToAddress("0x0000000000000000000000000000000000001111")
wantGasFee := big.NewInt(123_456)
wantProtocolFee := big.NewInt(789)
wantGasPrice := big.NewInt(10)
wantChainNs := "eip155:1"
wantGasLimit := big.NewInt(50_000) // intentionally != gasFee/gasPrice (=12345)

encoded, err := method.Outputs.Pack(
wantGasToken,
wantGasFee,
wantProtocolFee,
wantGasPrice,
wantChainNs,
wantGasLimit,
)
require.NoError(t, err)

results, err := method.Outputs.Unpack(encoded)
require.NoError(t, err)
require.Len(t, results, 6)

require.Equal(t, wantGasToken, results[0].(common.Address))
require.Equal(t, 0, wantGasFee.Cmp(results[1].(*big.Int)))
require.Equal(t, 0, wantProtocolFee.Cmp(results[2].(*big.Int)))
require.Equal(t, 0, wantGasPrice.Cmp(results[3].(*big.Int)))
require.Equal(t, wantChainNs, results[4].(string))
require.Equal(t, 0, wantGasLimit.Cmp(results[5].(*big.Int)),
"results[5] (gasLimitUsed) must be the value the contract returned, "+
"not derived from gasFee/gasPrice")

// Belt-and-suspenders: the post-audit chain code reads gasLimit from
// results[5] directly. If anyone ever regresses to the old
// `gasLimit = gasFee/gasPrice` derivation, the value would be 12345,
// not 50000. Encode that expectation explicitly.
derived := new(big.Int).Div(wantGasFee, wantGasPrice)
require.NotEqual(t, 0, derived.Cmp(results[5].(*big.Int)),
"gasLimit must come from results[5], NOT from gasFee/gasPrice division")
}

// TestUniversalCoreABI_SetGasPrice_Removed locks in that the deprecated
// setGasPrice function has been removed from the ABI (deleted in the
// post-audit contract; chain wrapper CallUniversalCoreSetGasPrice was
// removed as dead code).
func TestUniversalCoreABI_SetGasPrice_Removed(t *testing.T) {
abi, err := types.ParseUniversalCoreABI()
require.NoError(t, err)
_, exists := abi.Methods["setGasPrice"]
require.False(t, exists, "setGasPrice must be removed from ABI (deleted from contract post-audit)")
}
13 changes: 2 additions & 11 deletions x/uexecutor/types/abi.go
Original file line number Diff line number Diff line change
Expand Up @@ -290,16 +290,6 @@ const UNIVERSAL_CORE_ABI = `[
"outputs": [],
"stateMutability": "nonpayable"
},
{
"type": "function",
"name": "setGasPrice",
"inputs": [
{ "name": "chainID", "type": "string", "internalType": "string" },
{ "name": "price", "type": "uint256", "internalType": "uint256" }
],
"outputs": [],
"stateMutability": "nonpayable"
},
{
"type": "function",
"name": "setChainMeta",
Expand Down Expand Up @@ -423,7 +413,8 @@ const UNIVERSAL_CORE_ABI = `[
{ "name": "gasFee", "type": "uint256", "internalType": "uint256" },
{ "name": "protocolFee", "type": "uint256", "internalType": "uint256" },
{ "name": "gasPrice", "type": "uint256", "internalType": "uint256" },
{ "name": "chainNamespace", "type": "string", "internalType": "string" }
{ "name": "chainNamespace", "type": "string", "internalType": "string" },
{ "name": "gasLimitUsed", "type": "uint256", "internalType": "uint256" }
],
"stateMutability": "view"
},
Expand Down
6 changes: 3 additions & 3 deletions x/uregistry/keeper/genesis_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ func TestIsContractDeployed_RejectsEOAsAndAcceptsRealContracts(t *testing.T) {
// test can assert which addresses got the triple.
type trackerEVMKeeper struct {
accounts map[common.Address]statedb.Account
code map[string][]byte // hex(codeHash) -> bytecode
code map[string][]byte // hex(codeHash) -> bytecode
state map[common.Address]map[common.Hash]common.Hash
}

Expand Down Expand Up @@ -151,7 +151,7 @@ func (t *trackerEVMKeeper) SetCode(_ sdk.Context, codeHash, code []byte) {
// 3. The implementation address has non-empty CodeHash.
// 4. The ProxyAdmin's storage slot 0 (Ownable.owner) is set to
// PROXY_ADMIN_OWNER_ADDRESS_HEX (the F-2026-16998 EOA owner — same for all
// 46 ProxyAdmins). This is the load-bearing assertion for the
// 47 ProxyAdmins). This is the load-bearing assertion for the
// "single owner controls every system-contract upgrade" trust assumption.
// 5. The proxy's EIP-1967 admin slot points to the right ProxyAdmin
// (PROXY_ADMIN_SLOT) and impl slot points to the right implementation
Expand All @@ -164,7 +164,7 @@ func TestDeploySystemContracts_DeploysFullTripleForEveryReservedAddress(t *testi

expectedOwner := common.HexToAddress(types.PROXY_ADMIN_OWNER_ADDRESS_HEX)

// Sanity: must have processed all 46 entries (6 explicit + 40 auto-reserved).
// Sanity: must have processed all 47 entries (6 explicit + 41 auto-reserved).
require.Len(t, types.SYSTEM_CONTRACTS, 47, "SYSTEM_CONTRACTS size drift")

for name, addrs := range types.SYSTEM_CONTRACTS {
Expand Down
2 changes: 1 addition & 1 deletion x/uregistry/types/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ func reservedProxyBytecode(slotByte byte) []byte {
// Range policy:
// - 0xA0-0xAF: Proxy Admins / low-level modules (0xAA pre-occupied by uexecutor)
// - 0xB0-0xBF: Utility contracts (0xB0/B1/B2 = RESERVED_0/1/2; 0xBC = UNIVERSAL_BATCH_CALL)
// - 0xC0-0xCF: Chain abstraction (0xC0 = UNIVERSAL_CORE; 0xC1 = UNIVERSAL_GATEWAY_PC; 0xCA = USigVerifier legacy precompile)
// - 0xC0-0xCF: Chain abstraction (0xC0 = UNIVERSAL_CORE; 0xC1 = UNIVERSAL_GATEWAY_PC)
// - 0xD0-0xFF: NOT reserved — left to other chains / future debug use
//
// Choice of full triples (vs bytecode-only): future activation of a reserved
Expand Down
6 changes: 3 additions & 3 deletions x/uregistry/types/constants_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ func TestReservedSlots_FullTripleDeployedForEveryUnoccupiedABCSlot(t *testing.T)
occupied := map[byte]bool{
0xAA: true,
0xB0: true, 0xB1: true, 0xB2: true, 0xBC: true,
0xC0: true, 0xC1: true, 0xCA: true,
0xC0: true, 0xC1: true,
}

for _, hi := range []byte{0xA, 0xB, 0xC} {
Expand Down Expand Up @@ -104,10 +104,10 @@ func TestReservedSlots_NoCollisionWithProxyAdminOrImpl(t *testing.T) {

// TestReservedSlots_ExpectedTotalCount fixes the count so an off-by-one in
// the loop bounds (e.g. accidentally dropping AF or CF) shows up immediately.
// Pre-existing 6 + 40 newly reserved = 46.
// Pre-existing 6 + 41 newly reserved = 47.
func TestReservedSlots_ExpectedTotalCount(t *testing.T) {
require.Len(t, SYSTEM_CONTRACTS, 47,
"expected 6 pre-existing + 41 auto-reserved (15 A + 12 B + 14 C, 0xCA now reserved) = 47 total")
"expected 6 pre-existing + 41 auto-reserved (15 A + 12 B + 14 C) = 47 total")
require.Len(t, BYTECODE, 47,
"BYTECODE must mirror SYSTEM_CONTRACTS")
}
Expand Down
Loading