Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
69 commits
Select commit Hold shift + click to select a range
c6d1085
e2e setup added
AryaLanjewar3005 Feb 19, 2026
2de7fd0
added a replace-addresses command to e2e-setup which replaces neccess…
AryaLanjewar3005 Feb 19, 2026
fdc56f8
fix: registered static precompiles in local-native setup e2e
AryaLanjewar3005 Feb 20, 2026
1abe216
refactor: e2e-setup now uses local-multi-validator setup instead of l…
AryaLanjewar3005 Feb 20, 2026
472db33
sdk-setup added
AryaLanjewar3005 Feb 24, 2026
c1f442f
changed core-contracts branch in .env.example
AryaLanjewar3005 Feb 24, 2026
c326607
Merge branch 'main' into feat/e2e-setup
0xNilesh Feb 25, 2026
15be6a6
fix: deploy-addresses.json related issues
AryaLanjewar3005 Feb 25, 2026
740d045
fix: made token-config deploy_addresses.json driven in e2e-setup
AryaLanjewar3005 Feb 25, 2026
464988e
fix : QoL change script added
AryaLanjewar3005 Feb 25, 2026
8461756
Added replace-addresses command + sdk setup command is updated to wir…
AryaLanjewar3005 Feb 25, 2026
d1b4cc2
fix: sdk-testing command in e2e-tests
AryaLanjewar3005 Feb 25, 2026
28450d9
Merge branch 'main' into feat/e2e-setup
0xNilesh Feb 25, 2026
2b6687c
arb, base, bsc and solana support added for e2e-tests
AryaLanjewar3005 Feb 27, 2026
8d7d33f
fix : arbitrum rpc url for monitoring gateway transaction in local-mu…
AryaLanjewar3005 Feb 27, 2026
2afe798
fix : e2e-tests setup : make replace-addresses calculates from the .e…
AryaLanjewar3005 Feb 27, 2026
b9dd0a4
Merge branch 'main' into feat/e2e-setup
0xNilesh Mar 5, 2026
97fec92
refactor
AryaLanjewar3005 Mar 5, 2026
f812853
fix: BNB RPC URL, env account funding, replace-addresses script
AryaLanjewar3005 Mar 5, 2026
b94a598
fix: e2e-tests token path fix
AryaLanjewar3005 Mar 5, 2026
3ac1885
feat: sdk automatically changes TESTNET to LOCALNET in account.ts
AryaLanjewar3005 Mar 5, 2026
fd59e6e
refactor: important addresses which were changed for e2e testing
AryaLanjewar3005 Mar 5, 2026
782bf3b
refactor: minor clean ups
AryaLanjewar3005 Mar 5, 2026
e2f4f6c
fix: Push localnet allocation in sdk
AryaLanjewar3005 Mar 5, 2026
ae14e68
refactor: minor cleaning
AryaLanjewar3005 Mar 5, 2026
132b5f9
fix: some more address adjustments
AryaLanjewar3005 Mar 5, 2026
9838c8e
local external chain environment setup command added to e2e-tests setup
AryaLanjewar3005 Mar 20, 2026
774b29f
feat: support to outbound_changes branch + local evm and svm simulation
AryaLanjewar3005 Mar 25, 2026
f29f150
Merge remote-tracking branch 'origin/feat/e2e-test-outbound' into rel…
AryaLanjewar3005 Mar 26, 2026
ac0ba6d
initial outbound testing setup
AryaLanjewar3005 Mar 27, 2026
711b88f
tss keygen removed temporarily
AryaLanjewar3005 Mar 27, 2026
a4f366d
local-multi-validator tss setup
AryaLanjewar3005 Mar 27, 2026
3707da0
USDT_BSC support added to e2e-outbound setup
AryaLanjewar3005 Mar 31, 2026
4c8d680
local-setup with tss keygen support
AryaLanjewar3005 Mar 31, 2026
a4882e8
fix: add: vault method (#202)
Aman035 Apr 3, 2026
30343ad
support for local-native and updated event identifiers
AryaLanjewar3005 Apr 5, 2026
aa6307a
feat: updated READMEs of core validator and specific modules
0xNilesh Apr 7, 2026
12e0865
feat: updated READMEs of core validator and specific modules
0xNilesh Apr 7, 2026
b1a0602
working outbound setup
AryaLanjewar3005 Apr 9, 2026
6277d7b
Inbound-Outbound e2e-setup
AryaLanjewar3005 Apr 9, 2026
91f09b8
fix: rpc retry logic (#204)
Aman035 Apr 14, 2026
a6001cd
E2E readme updated
AryaLanjewar3005 Apr 15, 2026
c84f9a9
feat: added new usdc configs for eth, base, arb sepolia chains
0xNilesh Apr 16, 2026
6f764e5
feat: MsgExecutePayload deploys UEA if UEA address has non-zero balance
0xNilesh Apr 16, 2026
286fe3b
tests: added integration tests for UEA deployment edge case in MsgExe…
0xNilesh Apr 16, 2026
a773429
Merge pull request #205 from pushchain/feat/execute-payload-deploy-uea
0xNilesh Apr 16, 2026
ecc5bbd
feat: added proto changes in FundMigration event
0xNilesh Apr 17, 2026
180488f
refactor: added generated protobuf
0xNilesh Apr 17, 2026
c1f6abd
feat: added abi changes for new mappings in UVCore
0xNilesh Apr 17, 2026
cbdb33f
feat: added changes in MsgInitiateFundMigration for tssFundMigrationG…
0xNilesh Apr 17, 2026
3cbce9c
feat: added consensus migration changes of utss module for this mappi…
0xNilesh Apr 17, 2026
32fb84b
tests: added integration tests for fund migration gas limit changes
0xNilesh Apr 17, 2026
a8ee6f7
refactor: added upgrade handler for fund migration fixes
0xNilesh Apr 17, 2026
b5b9e64
Bug fixes and README instructions updated
AryaLanjewar3005 Apr 19, 2026
fb9d33f
quick-testing outbound feature added
AryaLanjewar3005 Apr 19, 2026
425c7ae
fix: event pasring with l1gasFee
Aman035 Apr 20, 2026
bced41f
fix: max transfer calculation
Aman035 Apr 20, 2026
5f7491d
fix: coordinator & sessionManager
Aman035 Apr 20, 2026
484b3b4
fix: broadcast fund migration
Aman035 Apr 20, 2026
ac9f2d1
Merge pull request #206 from pushchain/feat/tss-fund-migration-fixes
0xNilesh Apr 21, 2026
e9c6867
fix: Fund migration vote (#209)
Aman035 Apr 24, 2026
af52f49
Merge remote-tracking branch 'origin/main' into fix/e2e-tests-setup
AryaLanjewar3005 Apr 26, 2026
44cf1fe
fix e2e setup for local outbound tests
AryaLanjewar3005 Apr 26, 2026
1f77a96
e2e-tests/configs dir update
AryaLanjewar3005 Apr 27, 2026
2ab1878
LOCAL_OUTBOUND_BASE_GAS_LIMIT added
AryaLanjewar3005 Apr 27, 2026
dd853ee
inbound patch added
AryaLanjewar3005 Apr 27, 2026
237de6a
quick-testing-outbound for svm and evm added
AryaLanjewar3005 Apr 27, 2026
f3068bd
Some solana related adjustments
AryaLanjewar3005 Apr 29, 2026
da1d3d7
Merge branch 'audit-fixes' into fix/e2e-tests-setup-inbound-patch
0xNilesh Apr 30, 2026
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
211 changes: 211 additions & 0 deletions DERIVED_TRANSACTIONS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
# Derived Transactions

A primitive added in Push Chain's EVM fork ([`github.com/pushchain/evm`](https://github.com/pushchain/evm), pinned via `replace` in `go.mod`) that lets a Cosmos SDK module produce a **real EVM transaction** — one that has a real receipt, real logs, and is fully observable through the JSON-RPC layer — instead of an internal "module call" that exists only inside the SDK.

The new EVM keeper method is `DerivedEVMCall`. Everywhere in the Push Chain codebase that needs to act on the EVM as a Cosmos module (mint PRC20s, write chain-meta, deploy a UEA, refund gas, ...) goes through this single entry point.

## Why It Exists

Stock cosmos-evm exposes `EVMKeeper.CallEVM`:

```go
func (k Keeper) CallEVM(
ctx sdk.Context,
abi abi.ABI,
from, contract common.Address,
commit bool,
method string,
args ...interface{},
) (*types.MsgEthereumTxResponse, error)
```

`CallEVM` is built for **internal queries**: a Cosmos module wants to read state from a contract or trigger a side effect, and the EVM layer treats it as a synthetic call. It's enough for read paths and lightweight writes, but it has hard limitations the moment a module needs to behave like a first-class EVM sender:

| Need | `CallEVM` |
|---|---|
| Send native value (`msg.value`) | not supported (always 0) |
| Set an explicit `gasLimit` | not supported |
| Bypass gas accounting for module-initiated work | not supported |
| Act as a module account (no private key) sending a real EVM tx | not supported |
| Issue multiple calls in the same block from the same sender without nonce collisions | not supported (nonce is read from state on every call) |
| Produce a JSON-RPC-visible receipt with hash, gas used, and logs | partial — the call exists, but doesn't surface as a normal EVM tx |

`DerivedEVMCall` is the fork's answer to all six.

## The API

```go
DerivedEVMCall(
ctx sdk.Context,
abi abi.ABI,
from, contract common.Address,
value, gasLimit *big.Int,
commit, gasless, isModuleSender bool,
manualNonce *uint64,
method string,
args ...interface{},
) (*types.MsgEthereumTxResponse, error)
```

Defined on the Push Chain `EVMKeeper` interface in [`x/uexecutor/types/expected_keepers.go`](./x/uexecutor/types/expected_keepers.go).

| Parameter | Purpose |
|---|---|
| `ctx` | SDK context — provides block, gas meter, store access |
| `abi` | Parsed contract ABI for encoding the call |
| `from` | The EVM address that will appear as the tx sender. Can be a derived user address or a module account address. |
| `contract` | Destination contract |
| `value` | Native value to attach (`*big.Int`, may be `nil` or `big.NewInt(0)`) |
| `gasLimit` | Explicit gas limit (`nil` -> use a sensible default). Critical for predictable receipts. |
| `commit` | `true` = real state-changing tx; `false` = simulation / static call |
| `gasless` | `true` = skip gas accounting entirely. Used when the call is initiated by the protocol itself and shouldn't bill any user. |
| `isModuleSender` | `true` = `from` is a Cosmos module account (no private key). The fork's signer logic uses a deterministic synthetic signature instead of requiring a real ECDSA signature. |
| `manualNonce` | If non-`nil`, the caller supplies the nonce explicitly. This is what makes "many EVM calls in one block from the same module" deterministic — see [Manual Nonce Management](#manual-nonce-management). |
| `method` + `args` | Standard ABI-encoded call data |

The return type is `*evmtypes.MsgEthereumTxResponse`, the same type a normal `MsgEthereumTx` produces. Concretely:

```go
receipt, err := k.evmKeeper.DerivedEVMCall(...)
// receipt.Hash -- 0x... tx hash, queryable via eth_getTransactionByHash
// receipt.GasUsed -- real gas used, observable in receipts
// receipt.Logs -- real EVM logs, indexable by event subscribers
// receipt.Ret -- ABI-encoded return data (for view-style commits)
```

## When to Use Each Mode

The Push Chain codebase uses two distinct call patterns. Both are visible in [`x/uexecutor/keeper/evm.go`](./x/uexecutor/keeper/evm.go).

### 1. User-derived sender (UEA-routed user actions)

When a user submits a `MsgExecutePayload`, the Cosmos signer is converted to its derived EVM address and the EVM call is issued from that address. The UEA contract is what authenticates the request via `verificationData`. UEA migration takes the same path — there is no separate migration message; an upgrade is just an `executePayload` whose payload calls the UEA's migration entry point.

```go
return k.evmKeeper.DerivedEVMCall(
ctx,
abi,
evmFromAddress, // user's derived EVM address
ueaAddr,
big.NewInt(0),
gasLimit,
true, // commit
false, // gasless = false (real user tx, gas should appear in receipt)
false, // isModuleSender = false
nil, // manualNonce = nil (read from state like a normal user)
"executeUniversalTx",
abiUniversalPayload,
verificationData,
)
```

Why not `CallEVM`? Two reasons:
- Real receipts. Universal Validators, indexers, and the JSON-RPC layer all need to see the tx as a normal Ethereum tx so they can observe gas used, status, and emitted events.
- Explicit `gasLimit`. The payload's gas budget must be enforceable; `CallEVM` doesn't accept one.

### 2. Module-as-sender (protocol-initiated EVM work)

When `x/uexecutor` itself needs to issue an EVM call (deposit PRC20s, push chain-meta, refund unused gas, ...) the sender is the `uexecutor` module account. Module accounts don't have private keys, so this would be impossible via a normal `MsgEthereumTx` — you can't sign one. `DerivedEVMCall` with `isModuleSender=true` solves it:

```go
ueModuleAccAddress, _ := k.GetUeModuleAddress(ctx)
nonce, _ := k.GetModuleAccountNonce(ctx)
_, _ = k.IncrementModuleAccountNonce(ctx)

return k.evmKeeper.DerivedEVMCall(
ctx,
abi,
ueModuleAccAddress, // module account as sender
handlerAddr,
big.NewInt(0),
nil,
true, // commit
false, // gasless = false (we still want gas in the receipt)
true, // isModuleSender = true
&nonce, // manualNonce = explicit
"depositPRC20Token",
prc20Address, amount, to,
)
```

The fork is responsible for synthesising a deterministic "signature" for the module account so the tx can be properly receipted and indexed without ever needing a real key to exist.

## Manual Nonce Management

Stock cosmos-evm reads the sender's nonce from EVM state on every call. That's fine for users (one user = one tx in flight at a time, the mempool serializes the rest), but it breaks for module accounts that may need to issue **several** EVM calls within the same block:

```
BeginBlock
uexecutor.handleInbound1
-> CallPRC20Deposit (nonce = ?)
-> CallUniversalCoreRefundUnusedGas (nonce = ?)
uexecutor.handleInbound2
-> CallPRC20DepositAutoSwap (nonce = ?)
EndBlock
```

If the keeper read the nonce from state for each of these, every call within the same block would see the same starting nonce — and they'd all collide. The fork's solution is the `manualNonce *uint64` argument: the caller passes its own counter, the fork honours it, and is responsible for incrementing it before the next call.

`x/uexecutor` keeps that counter in its own KV store as the `ModuleAccountNonce` collection ([`x/uexecutor/keeper/keeper.go`](./x/uexecutor/keeper/keeper.go)):

```go
nonce, err := k.GetModuleAccountNonce(ctx) // read
if _, err := k.IncrementModuleAccountNonce(ctx); err != nil {
return nil, err
}
// pass &nonce to DerivedEVMCall
```

The increment happens **before** the call, intentionally — if the EVM call fails, the nonce gap is benign (skipped nonces are fine in EVM), but a post-call increment would risk reusing a nonce on retry. This pre-increment is the canonical way to issue derived txs from a module.

> ⚠️ **Single source of truth.** Only one collection in the whole codebase should ever increment `ModuleAccountNonce`. If two modules need to send derived txs as the same module account, they must coordinate through a single keeper helper. The current design has only `x/uexecutor` doing this, so the invariant holds trivially.

## The `gasless` Flag

`gasless=true` tells the fork: "this call is part of internal protocol bookkeeping, don't bill any account for the gas." Right now, every Push Chain call site passes `gasless=false`, with the inline comment:

> `// gasless = false (@dev: we need gas to be emitted in the tx receipt)`

The reason: even though the protocol pays the gas, the tx receipt still needs `gas_used` populated so off-chain services (Universal Validators, explorers, the gas-fee accounting in `x/uexecutor`) can read it back. Setting `gasless=true` would suppress the gas field and break that read path.

The flag exists for future use — protocol housekeeping calls that don't need to be observable via receipts (e.g. genesis-time bytecode patches). For day-to-day inbound/outbound execution, `gasless` stays `false`.

## Where It's Used

Every derived call in Push Chain is in [`x/uexecutor/keeper/evm.go`](./x/uexecutor/keeper/evm.go). Quick map:

| Helper | Sender | Why derived? |
|---|---|---|
| `CallFactoryToDeployUEA` | user-derived | Real tx receipt is required for the deploy; the deployer address is the source-chain user's derived EVM address. |
| `CallUEAExecutePayload` | user-derived | Carries `gasLimit` from the payload; receipt is consumed by the Universal Validator vote-back path. UEA migration also flows through this path now (the migration is just a payload that calls the UEA's migrate entry point). |
| `CallPRC20Deposit` | module | Mints PRC20 to recipient. Module account has no key. |
| `CallPRC20DepositAutoSwap` | module | Same, but with the auto-swap leg. |
| `CallUniversalCoreSetGasPrice` | module | Writes a single chain's gas price to the on-chain oracle. |
| `CallUniversalCoreSetChainMeta` | module | Writes gas price + block height for a chain. |
| `CallUniversalCoreRefundUnusedGas` | module | Refunds unused gas (with optional swap back to PC). |
| `CallExecuteUniversalTx` | module | Calls `executeUniversalTx` on a recipient smart contract for `isCEA` inbounds. |

The pure read paths in the same file (`CallFactoryToGetUEAAddressForOrigin`, `CallFactoryGetOriginForUEA`, `CallUEADomainSeparator`, `GetGasPriceByChain`, `GetUniversalCoreQuoterAddress`, `GetUniversalCoreWPCAddress`, `GetDefaultFeeTierForToken`, `GetSwapQuote`) all use plain `CallEVM` with `commit=false` — they don't need a receipt because they're static.

## Quick Reference: `CallEVM` vs `DerivedEVMCall`

```
CallEVM DerivedEVMCall
------- ---------------
value 0 (implicit) explicit *big.Int
gasLimit default explicit *big.Int (or nil)
commit yes yes
gasless no (always charges) flag (default: false in PC)
isModuleSender no flag (true = synthetic signer)
manualNonce no (read from state) optional override
JSON-RPC visible receipt partial yes — same as a user MsgEthereumTx
typical use internal queries, protocol-as-sender writes,
lightweight side effects user-derived EVM-routed actions
```

## Caveats

- **`isModuleSender=true` requires the synthetic signer logic in the fork.** If the upstream cosmos-evm version is bumped, that signer path must remain intact, otherwise module-originated derived calls will fail validation.
- **`manualNonce` is the caller's responsibility.** The fork trusts the supplied value verbatim. Two callers stomping each other's nonce will cause receipt collisions and confusing replays.
- **Pre-increment, never post-increment.** If you increment after the call and the call panics or errors mid-execution, you've now reused a nonce. Always increment first; treat skipped nonces as a non-issue (EVM allows nonce gaps for module accounts since no transaction sequencing depends on them).
- **`gasless=true` suppresses the gas field in the receipt.** Until there's a clear reason to drop receipts on the floor for a particular call site, leave it `false`.
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,10 @@ draw-deps:
clean:
rm -rf snapcraft-local.yaml build/

.PHONY: replace-addresses
replace-addresses:
bash e2e-tests/replace_addresses.sh

distclean: clean
rm -rf vendor/

Expand Down
Loading
Loading