Skip to content
Open
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
6 changes: 4 additions & 2 deletions .github/workflows/linters.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Commit Lint Dependencies
run: npm install @commitlint/config-conventional
- uses: actions/setup-node@v4
with:
node-version: 24.x
- run: npm ci --ignore-scripts
- uses: JulienKode/pull-request-name-linter-action@v0.5.0
7 changes: 7 additions & 0 deletions .github/workflows/pull_requests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ on:
pull_request:
types: [opened, reopened, synchronize]

permissions:
contents: read

env:
NODE_ENV: ci

Expand All @@ -24,6 +27,10 @@ jobs:
pull-requests: write
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 24.x
- run: npm ci --ignore-scripts
- uses: reviewdog/action-eslint@v1
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ on:
- main
- dev

permissions:
contents: read

env:
NODE_ENV: ci

Expand Down
50 changes: 44 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,16 +138,54 @@ npx hardhat run scripts/mintDocumentGasless.ts --network sepolia

## Environment variables

Deploy/registry/paymaster addresses are network-scoped — suffix the variable with `_SEPOLIA` or `_AMOY` (e.g. `FACTORY_ADDRESS_SEPOLIA`, `FACTORY_ADDRESS_AMOY`). `scripts/lib/network.ts` resolves the suffix from `--network <name>` (hardhat scripts) or the `NETWORK` env var (viem/permissionless scripts). See `.env.example` for the full annotated list.

### Wallets & RPC

| Variable | Description |
| --- | --- |
| `PRIVATE_KEY` | Deployer wallet private key |
| `PRIVATE_KEY` | Deployer/gas-payer wallet — pays for deployments, delegation txs, staking |
| `PRIVATE_KEY2` | Secondary wallet (optional — testing with a second account) |
| `OWNER_PRIVATE_KEY` | Platform owner / whitelisted user — signs UserOps, needs no ETH for gasless ops |
| `SEPOLIA_RPC_URL` | Sepolia RPC endpoint |
| `AMOY_RPC_URL` | Polygon Amoy RPC endpoint |
| `PIMLICO_API_KEY` | Pimlico bundler API key |
| `TDOC_DEPLOYER_ADDRESS` | Deployed TDocDeployer address |
| `PAYMASTER_IMPLEMENTATION` | PlatformPaymaster implementation address |
| `FACTORY_ADDRESS` | PlatformAccountFactory address |
| `PAYMASTER_ADDRESS` | Deployed paymaster clone address |
| `EIP7702_IMPL_ADDRESS` | EIP7702Implementation address |
| `NETWORK` | `sepolia` \| `amoy` — target network for viem/permissionless scripts (default: `sepolia`) |
| `ENTRY_POINT` | EntryPoint v0.8 address (default: `0x4337084D9E255Ff0702461CF8895CE9E3b5Ff108`, same on all supported chains) |

### Deployed addresses (network-suffixed)

| Variable | Description |
| --- | --- |
| `EIP7702_IMPL_ADDRESS_<NETWORK>` | Deployed `EIP7702Implementation` address |
| `PAYMASTER_IMPLEMENTATION_<NETWORK>` | Deployed `PlatformPaymaster` implementation address |
| `FACTORY_ADDRESS_<NETWORK>` | Deployed `PlatformAccountFactory` address |
| `PAYMASTER_ADDRESS_<NETWORK>` | Deployed paymaster clone address |
| `TDOC_DEPLOYER_ADDRESS_<NETWORK>` | TrustVC `TDocDeployer` address (pre-deployed infra) |
| `TDOC_IMPLEMENTATION_<NETWORK>` | TDoc implementation to clone via `deployRegistry` |
| `REGISTRY_ADDRESS_<NETWORK>` | Registry deployed via `deployRegistryGasless.ts` |
| `TITLE_ESCROW_ADDRESS_<NETWORK>` | Title escrow captured via `mintDocumentGasless.ts` |

### Gasless script inputs

| Variable | Description |
| --- | --- |
| `TOKEN_NAME` / `TOKEN_SYMBOL` | Name/symbol for the TradeTrust token registry (`deployRegistryGasless.ts`) |
| `TOKEN_ID` | Document token ID as `uint256` (`mintDocumentGasless.ts`) |
| `BENEFICIARY_ADDRESS` / `HOLDER_ADDRESS` | Document beneficiary/holder (`mintDocumentGasless.ts`) |
| `REMARK` | Optional remark bytes/text attached to the document |
| `NOMINEE_ADDR` / `NEW_HOLDER_ADDR` | Used by the `scripts/trFunctions/*` title-escrow helpers |

### Optional deploy/stake overrides

| Variable | Description |
| --- | --- |
| `PLATFORM_ADDRESS` | Paymaster owner EOA for `deployPlatformPaymaster.ts` (default: deployer) |
| `DAILY_LIMIT_ETH` | Per-user daily gas limit in ETH (default: `0` = unlimited) |
| `DEPLOY_SALT` | Hex `bytes32` CREATE2 salt (default: random) |
| `STAKE_AMOUNT_ETH` | ETH locked as EntryPoint stake (default: `0.01`) |
| `DEPOSIT_AMOUNT_ETH` | ETH deposited into the gas pool (default: `0.05`) |
| `UNSTAKE_DELAY_SEC` | Stake lock period in seconds (default: `86400` = 1 day) |

## Tech stack

Expand Down
10 changes: 2 additions & 8 deletions contracts/Factory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol";
contract PlatformAccountFactory is Ownable {
address public tdocDeployer;
address public paymasterImplementation;
mapping(address => address) public attachedPaymaster;

event PlatformOnboarded(
address indexed platformAddress,
Expand All @@ -21,17 +20,12 @@ contract PlatformAccountFactory is Ownable {
address _tdocDeployer,
address _paymasterImplementation
) Ownable(msg.sender) {
require(_tdocDeployer != address(0), "Zero address");
require(_paymasterImplementation != address(0), "Zero address");
tdocDeployer = _tdocDeployer;
paymasterImplementation = _paymasterImplementation;
}

function setAttachedPaymaster(
address platformAddress
) external view returns (address) {
address paymaster = attachedPaymaster[platformAddress];
return paymaster;
}

function updateTdocDeployer(address _tdocDeployer) external onlyOwner {
require(_tdocDeployer != address(0), "Zero address");
tdocDeployer = _tdocDeployer;
Expand Down
19 changes: 18 additions & 1 deletion contracts/PlatformPaymaster.sol
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,12 @@ contract PlatformPaymaster is BasePaymaster {
bytes calldata remark
) external returns (address titleEscrow) {
require(authorizedRegistries[registry], "registry not authorized");
require(
authorizedCallers[msg.sender] ||
userWhitelist[msg.sender] > 0 ||
msg.sender == owner(),
"caller not authorized"
);

titleEscrow = ITradeTrustToken(registry).mint(
beneficiary,
Expand Down Expand Up @@ -291,7 +297,18 @@ contract PlatformPaymaster is BasePaymaster {
}

if (innerSel == MINT_DOCUMENT_SEL) {
// mintDocument: registry enforces MINTER_ROLE — no extra whitelist needed
if (
!authorizedCallers[sender] &&
userWhitelist[sender] == 0 &&
sender != owner()
) {
emit UserOpRejected(sender, "caller not authorized");
return ("", _packValidationData(true, 0, 0));
}
if (dailyLimit > 0 && dailySpend[sender] + maxCost > dailyLimit) {
emit UserOpRejected(sender, "daily limit exceeded");
return ("", _packValidationData(true, 0, 0));
}
return (
abi.encode(sender, maxCost, false),
_packValidationData(false, 0, 0)
Expand Down
3 changes: 3 additions & 0 deletions contracts/mocks/MockRegistry.sol
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ contract MockRegistry {
uint256,
bytes calldata
) external returns (address titleEscrow) {
if (!_roles[MINTER_ROLE][msg.sender]) {
revert AccessControlUnauthorizedAccount(msg.sender, MINTER_ROLE);
}
titleEscrow = address(new MockTitleEscrow());
lastTitleEscrow = titleEscrow;
}
Expand Down
3 changes: 2 additions & 1 deletion scripts/deployFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { createPublicClient, createWalletClient, http } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import hre from "hardhat";
import * as dotenv from "dotenv";
import { getNetworkConfig, getEnv } from "./lib/network";
import { getNetworkConfig, getEnv, getFeeOverrides } from "./lib/network";
dotenv.config();

async function main() {
Expand Down Expand Up @@ -44,6 +44,7 @@ async function main() {
abi: artifact.abi,
bytecode: artifact.bytecode as `0x${string}`,
args: [tdocDeployer, paymasterImpl],
...getFeeOverrides(hre.network.name),
});
console.log(" tx:", txHash);

Expand Down
3 changes: 2 additions & 1 deletion scripts/deployImplementation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { createPublicClient, createWalletClient, http } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import hre from "hardhat";
import * as dotenv from "dotenv";
import { getNetworkConfig } from "./lib/network";
import { getNetworkConfig, getFeeOverrides } from "./lib/network";
dotenv.config();

const DEFAULT_ENTRY_POINT = "0x4337084D9E255Ff0702461CF8895CE9E3b5Ff108" as `0x${string}`;
Expand Down Expand Up @@ -45,6 +45,7 @@ async function main() {
abi: artifact.abi,
bytecode: artifact.bytecode as `0x${string}`,
args: [entryPoint],
...getFeeOverrides(hre.network.name),
});
console.log(" tx:", txHash);

Expand Down
3 changes: 2 additions & 1 deletion scripts/deployPlatformPaymaster.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import { randomBytes } from "crypto";
import { privateKeyToAccount } from "viem/accounts";
import hre from "hardhat";
import * as dotenv from "dotenv";
import { getNetworkConfig, getEnv } from "./lib/network";
import { getNetworkConfig, getEnv, getFeeOverrides } from "./lib/network";
dotenv.config();

const factoryAbi = parseAbi([
Expand Down Expand Up @@ -64,6 +64,7 @@ async function main() {
abi: factoryAbi,
functionName: "deployPlatformPaymaster",
args: [platformAddress, dailyLimit, salt],
...getFeeOverrides(hre.network.name),
});
console.log(" tx:", txHash);

Expand Down
18 changes: 18 additions & 0 deletions scripts/lib/network.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// network is added to hardhat.config.ts.

import { sepolia, polygonAmoy } from "viem/chains";
import { parseGwei } from "viem";
import type { Chain } from "viem";

interface NetworkEntry {
Expand Down Expand Up @@ -41,3 +42,20 @@ export function getEnv(suffix: string, name: string, required = true): string {
if (!val && required) throw new Error(`${key} is not set in .env`);
return val ?? "";
}

/**
* Amoy's Infura endpoint has been observed returning a broken EIP-1559 fee
* suggestion (maxPriorityFeePerGas ≈ maxFeePerGas, leaving ~0 margin for the
* base fee), which makes eth_estimateGas reject the call outright. Override
* with fixed fees on Amoy to bypass viem's automatic estimation; Sepolia's
* estimation isn't known to have this problem, so leave it untouched there.
*/
export function getFeeOverrides(
networkName: string,
): { maxFeePerGas: bigint; maxPriorityFeePerGas: bigint } | undefined {
if (networkName !== "amoy") return undefined;
return {
maxFeePerGas: parseGwei(process.env.AMOY_MAX_FEE_GWEI ?? "100"),
maxPriorityFeePerGas: parseGwei(process.env.AMOY_MAX_PRIORITY_FEE_GWEI ?? "30"),
};
Comment on lines +53 to +60

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 3 \
  'AMOY_MAX_FEE_GWEI|AMOY_MAX_PRIORITY_FEE_GWEI|getFeeOverrides' \
  scripts README.md .env.example 2>/dev/null || true

Repository: TrustVC/7702SmartAccount

Length of output: 3736


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- scripts/lib/network.ts ---'
cat -n scripts/lib/network.ts | sed -n '1,90p'

printf '%s\n' '--- package manifests and lockfiles ---'
git ls-files | rg '(^|/)(package\.json|pnpm-lock\.yaml|yarn\.lock|package-lock\.json|README\.md|\.env\.example)$' | head -80

printf '%s\n' '--- fee configuration documentation and validation ---'
rg -n -C 4 \
  'AMOY_MAX_FEE_GWEI|AMOY_MAX_PRIORITY_FEE_GWEI|maxFeePerGas|maxPriorityFeePerGas|parseGwei|fee overrides' \
  --glob '!node_modules/**' --glob '!dist/**' --glob '!build/**' .

printf '%s\n' '--- deployment call context ---'
for f in scripts/deployImplementation.ts scripts/deployFactory.ts scripts/deployPlatformPaymaster.ts; do
  if [ -f "$f" ]; then
    echo "### $f"
    cat -n "$f" | sed -n '35,75p'
  fi
done

Repository: TrustVC/7702SmartAccount

Length of output: 50383


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path

# Read repository files as data. This probe models the EIP-1559 invariant directly
# without importing or executing repository code.
values = [
    ("defaults", "100", "30"),
    ("priority below max", "100", "30"),
    ("priority equal to max", "100", "100"),
    ("priority above max", "30", "100"),
]
for label, max_fee, priority_fee in values:
    valid = int(priority_fee) <= int(max_fee)
    print(f"{label}: maxFeePerGas={max_fee} gwei, maxPriorityFeePerGas={priority_fee} gwei, valid={valid}")

print("\nRelevant package declarations:")
for path in sorted(Path(".").glob("**/package.json")):
    if "node_modules" not in path.parts:
        text = path.read_text(errors="replace")
        if "viem" in text or path.name == "package.json":
            print(path)
            for line in text.splitlines():
                if "viem" in line or '"type"' in line:
                    print(line.strip())
PY

printf '%s\n' '--- exact tracked configuration references ---'
git grep -n -E 'AMOY_MAX_FEE_GWEI|AMOY_MAX_PRIORITY_FEE_GWEI|maxFeePerGas|maxPriorityFeePerGas' -- ':!package-lock.json' ':!pnpm-lock.yaml' ':!yarn.lock' || true

Repository: TrustVC/7702SmartAccount

Length of output: 50381


🌐 Web query:

EIP-1559 transaction validity max_fee_per_gas max_priority_fee_per_gas priority fee greater than max fee

💡 Result:

For EIP-1559 (Type 2) transactions on the Ethereum network, it is a strict validity requirement that the max_fee_per_gas must be greater than or equal to the max_priority_fee_per_gas [1][2][3]. Specifically, the protocol enforces the following validation constraint [1][3]: assert transaction.max_fee_per_gas >= transaction.max_priority_fee_per_gas If a transaction is submitted where max_priority_fee_per_gas exceeds max_fee_per_gas, it is considered invalid and will be rejected by the network [4][5]. The fundamental roles of these parameters are: 1. max_fee_per_gas: The absolute maximum fee per unit of gas the user is willing to pay (covering both the base fee and the priority fee) [1][6]. 2. max_priority_fee_per_gas: The maximum tip the user is willing to pay to the validator [1][6]. Because the total cost to the user is the sum of the protocol-defined base fee and the priority fee (capped by the max_fee_per_gas), the transaction must be structured such that the total maximum budget is at least as large as the priority component [1][7][3]. Additionally, for a transaction to be valid, the max_fee_per_gas must also be greater than or equal to the current block's base fee [1][3]. If the sum of the base fee and the desired priority fee exceeds the max_fee_per_gas, the priority fee is effectively capped at (max_fee_per_gas - base_fee_per_gas) to ensure the transaction remains within the user's specified maximum budget [7][3].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

cat -n scripts/lib/network.ts | sed -n '40,70p'

printf '%s\n' '--- fee-related declarations and documentation ---'
git grep -n -E 'AMOY_MAX_FEE_GWEI|AMOY_MAX_PRIORITY_FEE_GWEI|maxFeePerGas|maxPriorityFeePerGas|parseGwei' -- \
  ':!package-lock.json' ':!pnpm-lock.yaml' ':!yarn.lock' || true

printf '%s\n' '--- package manifests containing viem ---'
while IFS= read -r f; do
  if grep -q '"viem"' "$f" 2>/dev/null; then
    echo "### $f"
    grep -n -C 2 '"viem"' "$f"
  fi
done < <(git ls-files '*package.json')

Repository: TrustVC/7702SmartAccount

Length of output: 50381


🌐 Web query:

EIP-1559 validity condition max_fee_per_gas max_priority_fee_per_gas transaction rejected

💡 Result:

Under EIP-1559, a transaction is considered invalid and will be rejected by the network if it fails to meet specific fee-related criteria [1][2]. The primary validity conditions concerning gas fees are as follows: 1. Max Fee vs. Base Fee: The transaction's max_fee_per_gas must be greater than or equal to the block's current base_fee_per_gas [1][3]. If max_fee_per_gas is less than the base fee, the transaction is rejected because the sender is not willing to pay the minimum required to include the transaction in the block [1][4]. 2. Max Fee vs. Priority Fee: The transaction's max_fee_per_gas must be greater than or equal to its max_priority_fee_per_gas [1][5][6]. The protocol requires this because the max fee represents the total amount the sender is willing to pay, which must cover both the priority fee (tip) and the base fee [1][4]. 3. Impossibly Large Values: Both max_fee_per_gas and max_priority_fee_per_gas must be less than 2^256 [1][2]. Values that exceed this limit are considered invalid [1][7]. 4. Sufficient Balance: Beyond the fee parameters, the sender's account balance must be sufficient to cover the "upfront cost" of the transaction [2][7]. This is calculated based on the gas_limit multiplied by the max_fee_per_gas, ensuring the sender can afford the transaction regardless of the actual base fee at the time of inclusion [2][5][7]. In practice, if a transaction meets these basic validity requirements, the effective gas price paid is determined by the formula: min(max_priority_fee_per_gas, max_fee_per_gas - base_fee_per_gas) + base_fee_per_gas [1][2]. This ensures that the user never pays more than their specified max_fee_per_gas [8][4].

Citations:


Reject invalid Amoy fee overrides.

When AMOY_MAX_PRIORITY_FEE_GWEI exceeds AMOY_MAX_FEE_GWEI, the EIP-1559 transaction is invalid. Throw before submitting the deployment transaction.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/lib/network.ts` around lines 53 - 60, Update getFeeOverrides to parse
the Amoy fee environment values and validate that maxPriorityFeePerGas does not
exceed maxFeePerGas. Throw an error before returning the overrides when the
priority fee is higher, while preserving the existing defaults and undefined
result for non-Amoy networks.

}
10 changes: 8 additions & 2 deletions src/constants/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
export const ChainId = {
Sepolia: 11155111,
Amoy: 80002,
} as const;

/** Deployed contract addresses indexed by chainId */
export const contractAddress = {
PaymasterImplementation: {
[ChainId.Sepolia]: "0x5ca5652025ca77d13323ed4887b4cbee6098dd8f",
[ChainId.Amoy]: "0xf47d58D3adc642DaD23966698A7A60b8b34D72f8",
},
PlatformAccountFactory: {
[ChainId.Sepolia]: "0x5dcDf7fA6Ab8323F67FD66E89b6CeD4564f9F4Ff",
[ChainId.Sepolia]: "0x1fe801f6af6e9a6c76431db08b121a7de70bc895",
[ChainId.Amoy]: "0x2762abf6fa22314ebcab41dd4666836038d29341",
},
} as const;
} as const;
Comment on lines +8 to +16

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

: "${SEPOLIA_RPC_URL:?Set SEPOLIA_RPC_URL}"
: "${AMOY_RPC_URL:?Set AMOY_RPC_URL}"

check_code() {
  local rpc_url="$1"
  local address="$2"
  local label="$3"

  local result
  result="$(curl -fsS "$rpc_url" \
    -H 'content-type: application/json' \
    --data "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"eth_getCode\",\"params\":[\"$address\",\"latest\"]}" \
    | jq -r '.result')"

  test "$result" != "0x" || {
    echo "No bytecode for $label at $address" >&2
    exit 1
  }
  echo "Verified bytecode: $label"
}

check_code "$SEPOLIA_RPC_URL" "0x5ca5652025ca77d13323ed4887b4cbee6098dd8f" "Sepolia PaymasterImplementation"
check_code "$SEPOLIA_RPC_URL" "0x1fe801f6af6e9a6c76431db08b121a7de70bc895" "Sepolia PlatformAccountFactory"
check_code "$AMOY_RPC_URL" "0xf47d58D3adc642DaD23966698A7A60b8b34D72f8" "Amoy PaymasterImplementation"
check_code "$AMOY_RPC_URL" "0x2762abf6fa22314ebcab41dd4666836038d29341" "Amoy PlatformAccountFactory"

Repository: TrustVC/7702SmartAccount

Length of output: 213


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- relevant repository files ---'
git ls-files | rg '(^|/)(src/constants/index\.ts|package\.json|README|.*artifact.*|.*deployment.*|.*deploy.*|.*factory.*|.*paymaster.*)' | head -200

printf '%s\n' '--- constants file ---'
cat -n src/constants/index.ts

printf '%s\n' '--- public RPC bytecode checks ---'
check_code() {
  local rpc_url="$1"
  local address="$2"
  local label="$3"
  local response result
  response="$(curl -fsS --max-time 20 "$rpc_url" \
    -H 'content-type: application/json' \
    --data "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"eth_getCode\",\"params\":[\"$address\",\"latest\"]}")"
  result="$(printf '%s' "$response" | jq -r '.result // empty')"
  if [ -z "$result" ]; then
    printf '%s: RPC error: %s\n' "$label" "$response" >&2
    return 1
  fi
  if [ "$result" = "0x" ]; then
    printf '%s: no bytecode at %s\n' "$label" "$address"
    return 1
  fi
  printf '%s: bytecode present (%s bytes)\n' "$label" "$(( (${`#result`} - 2) / 2 ))"
}

check_code "https://ethereum-sepolia-rpc.publicnode.com" \
  "0x5ca5652025ca77d13323ed4887b4cbee6098dd8f" "Sepolia PaymasterImplementation"
check_code "https://ethereum-sepolia-rpc.publicnode.com" \
  "0x1fe801f6af6e9a6c76431db08b121a7de70bc895" "Sepolia PlatformAccountFactory"
check_code "https://polygon-amoy-bor-rpc.publicnode.com" \
  "0xf47d58D3adc642DaD23966698A7A60b8b34D72f8" "Amoy PaymasterImplementation"
check_code "https://polygon-amoy-bor-rpc.publicnode.com" \
  "0x2762abf6fa22314ebcab41dd4666836038d29341" "Amoy PlatformAccountFactory"

Repository: TrustVC/7702SmartAccount

Length of output: 1760


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- deployment metadata ---'
cat ignition/deployments/chain-11155111/deployed_addresses.json
printf '%s\n' '--- factory artifact metadata ---'
jq '{contractName, sourceName, bytecodeLength:(.bytecode|length), deployedBytecodeLength:(.deployedBytecode|length), deployedBytecodeHash:(.deployedBytecode|sha256)}' \
  ignition/deployments/chain-11155111/artifacts/FactoryModule#PlatformAccountFactory.json

printf '%s\n' '--- contract and deployment references ---'
git ls-files | rg '\.(sol|ts|json)$' | sort | head -300
rg -n 'PaymasterImplementation|PlatformAccountFactory|5ca565|f47d58|1fe801|2762ab|deployedBytecode|bytecode' \
  --glob '!package-lock.json' --glob '!yarn.lock' .

printf '%s\n' '--- deployed runtime bytecode hashes from public RPCs ---'
get_code() {
  local rpc_url="$1"
  local address="$2"
  curl -fsS --max-time 20 "$rpc_url" \
    -H 'content-type: application/json' \
    --data "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"eth_getCode\",\"params\":[\"$address\",\"latest\"]}" \
    | jq -r '.result'
}
for item in \
  "https://ethereum-sepolia-rpc.publicnode.com 0x5ca5652025ca77d13323ed4887b4cbee6098dd8f Sepolia-PaymasterImplementation" \
  "https://ethereum-sepolia-rpc.publicnode.com 0x1fe801f6af6e9a6c76431db08b121a7de70bc895 Sepolia-PlatformAccountFactory" \
  "https://polygon-amoy-bor-rpc.publicnode.com 0xf47d58D3adc642DaD23966698A7A60b8b34D72f8 Amoy-PaymasterImplementation" \
  "https://polygon-amoy-bor-rpc.publicnode.com 0x2762abf6fa22314ebcab41dd4666836038d29341 Amoy-PlatformAccountFactory"
do
  set -- $item
  code="$(get_code "$1" "$2")"
  printf '%s length=%s sha256=' "$3" "${`#code`}"
  printf '%s' "$code" | sha256sum | cut -d' ' -f1
done

Repository: TrustVC/7702SmartAccount

Length of output: 736


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- factory artifact metadata ---'
jq '{contractName, sourceName, bytecodeLength:(.bytecode|length), deployedBytecodeLength:(.deployedBytecode|length)}' \
  'ignition/deployments/chain-11155111/artifacts/FactoryModule#PlatformAccountFactory.json'

printf '%s\n' '--- deployment and address references ---'
rg -n 'PaymasterImplementation|PlatformAccountFactory|5ca565|f47d58|1fe801|2762ab|deployedBytecode|bytecode' \
  --glob '!package-lock.json' --glob '!yarn.lock' .

printf '%s\n' '--- deployed runtime bytecode hashes from public RPCs ---'
python3 - <<'PY'
import hashlib
import json
import ssl
import urllib.request

checks = [
    ("https://ethereum-sepolia-rpc.publicnode.com", "0x5ca5652025ca77d13323ed4887b4cbee6098dd8f", "Sepolia PaymasterImplementation"),
    ("https://ethereum-sepolia-rpc.publicnode.com", "0x1fe801f6af6e9a6c76431db08b121a7de70bc895", "Sepolia PlatformAccountFactory"),
    ("https://polygon-amoy-bor-rpc.publicnode.com", "0xf47d58D3adc642DaD23966698A7A60b8b34D72f8", "Amoy PaymasterImplementation"),
    ("https://polygon-amoy-bor-rpc.publicnode.com", "0x2762abf6fa22314ebcab41dd4666836038d29341", "Amoy PlatformAccountFactory"),
]
ctx = ssl.create_default_context()
for rpc, address, label in checks:
    body = json.dumps({
        "jsonrpc": "2.0",
        "id": 1,
        "method": "eth_getCode",
        "params": [address, "latest"],
    }).encode()
    request = urllib.request.Request(
        rpc, data=body, headers={"content-type": "application/json"}
    )
    with urllib.request.urlopen(request, timeout=20, context=ctx) as response:
        result = json.load(response)["result"]
    print(f"{label}: length={len(result)} sha256={hashlib.sha256(result.encode()).hexdigest()}")
PY

Repository: TrustVC/7702SmartAccount

Length of output: 50381


🏁 Script executed:

#!/bin/bash
set -euo pipefail

log=/tmp/coderabbit-shell-logs/shell-output-vXf3te
printf '%s\n' '--- command errors ---'
rg -n -i 'error|failed|exception|timeout|urlopen|traceback|rpc' "$log" | head -100 || true

printf '%s\n' '--- concise artifact metadata ---'
python3 - <<'PY'
import json
from pathlib import Path

path = Path("ignition/deployments/chain-11155111/artifacts/FactoryModule#PlatformAccountFactory.json")
data = json.loads(path.read_text())
for key in ("contractName", "sourceName", "bytecode", "deployedBytecode"):
    value = data.get(key)
    if isinstance(value, str):
        print(f"{key}: length={len(value)}")
    else:
        print(f"{key}: {value}")
PY

printf '%s\n' '--- deployment/address references excluding uploaded bytecode ---'
rg -n 'PaymasterImplementation|PlatformAccountFactory|5ca565|f47d58|1fe801|2762ab|deployedBytecode|bytecode' \
  src scripts ignition README.md package.json 2>/dev/null | head -200 || true

printf '%s\n' '--- deployed runtime bytecode lengths and hashes ---'
python3 - <<'PY'
import hashlib, json, ssl, urllib.request

checks = [
    ("https://ethereum-sepolia-rpc.publicnode.com", "0x5ca5652025ca77d13323ed4887b4cbee6098dd8f", "Sepolia PaymasterImplementation"),
    ("https://ethereum-sepolia-rpc.publicnode.com", "0x1fe801f6af6e9a6c76431db08b121a7de70bc895", "Sepolia PlatformAccountFactory"),
    ("https://polygon-amoy-bor-rpc.publicnode.com", "0xf47d58D3adc642DaD23966698A7A60b8b34D72f8", "Amoy PaymasterImplementation"),
    ("https://polygon-amoy-bor-rpc.publicnode.com", "0x2762abf6fa22314ebcab41dd4666836038d29341", "Amoy PlatformAccountFactory"),
]
for rpc, address, label in checks:
    body = json.dumps({"jsonrpc":"2.0","id":1,"method":"eth_getCode","params":[address,"latest"]}).encode()
    req = urllib.request.Request(rpc, data=body, headers={"content-type":"application/json"})
    try:
        with urllib.request.urlopen(req, timeout=20) as response:
            result = json.load(response).get("result")
        print(f"{label}: length={len(result)} sha256={hashlib.sha256(result.encode()).hexdigest()}")
    except Exception as exc:
        print(f"{label}: request failed: {exc}")
PY

Repository: TrustVC/7702SmartAccount

Length of output: 50381


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
import hashlib
import json
import urllib.request
from pathlib import Path

artifact_path = Path("ignition/deployments/chain-11155111/artifacts/FactoryModule#PlatformAccountFactory.json")
artifact = json.loads(artifact_path.read_text())
print("artifact keys:", ",".join(sorted(artifact.keys())))
for key in ("bytecode", "deployedBytecode"):
    value = artifact.get(key, "")
    print(f"artifact {key}: chars={len(value)} bytes={(len(value)-2)//2 if value.startswith('0x') else 'n/a'} sha256={hashlib.sha256(value.encode()).hexdigest()}")

checks = [
    ("https://ethereum-sepolia-rpc.publicnode.com", "0x5ca5652025ca77d13323ed4887b4cbee6098dd8f", "Sepolia PaymasterImplementation"),
    ("https://ethereum-sepolia-rpc.publicnode.com", "0x1fe801f6af6e9a6c76431db08b121a7de70bc895", "Sepolia PlatformAccountFactory"),
    ("https://polygon-amoy-bor-rpc.publicnode.com", "0xf47d58D3adc642DaD23966698A7A60b8b34D72f8", "Amoy PaymasterImplementation"),
    ("https://polygon-amoy-bor-rpc.publicnode.com", "0x2762abf6fa22314ebcab41dd4666836038d29341", "Amoy PlatformAccountFactory"),
]
for rpc, address, label in checks:
    body = json.dumps({"jsonrpc":"2.0","id":1,"method":"eth_getCode","params":[address,"latest"]}).encode()
    req = urllib.request.Request(rpc, data=body, headers={"content-type":"application/json"})
    with urllib.request.urlopen(req, timeout=20) as response:
        result = json.load(response)["result"]
    print(f"{label}: chars={len(result)} bytes={(len(result)-2)//2} sha256={hashlib.sha256(result.encode()).hexdigest()}")
    if "PlatformAccountFactory" in label:
        deployed = artifact.get("deployedBytecode", "")
        print(f"{label}: exact_artifact_match={result.lower() == deployed.lower()}")
        print(f"{label}: artifact_in_chain_code={deployed.lower().removeprefix('0x') in result.lower()}")
PY

Repository: TrustVC/7702SmartAccount

Length of output: 2802


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
import hashlib
import json
import subprocess
from pathlib import Path

artifact = json.loads(Path("ignition/deployments/chain-11155111/artifacts/FactoryModule#PlatformAccountFactory.json").read_text())
expected = artifact["deployedBytecode"].lower()

checks = [
    ("https://ethereum-sepolia-rpc.publicnode.com", "0x5ca5652025ca77d13323ed4887b4cbee6098dd8f", "Sepolia PaymasterImplementation"),
    ("https://ethereum-sepolia-rpc.publicnode.com", "0x1fe801f6af6e9a6c76431db08b121a7de70bc895", "Sepolia configured PlatformAccountFactory"),
    ("https://ethereum-sepolia-rpc.publicnode.com", "0x5dcDf7fA6Ab8323F67FD66E89b6CeD4564f9F4Ff", "Sepolia deployment-metadata PlatformAccountFactory"),
    ("https://polygon-amoy-bor-rpc.publicnode.com", "0xf47d58D3adc642DaD23966698A7A60b8b34D72f8", "Amoy PaymasterImplementation"),
    ("https://polygon-amoy-bor-rpc.publicnode.com", "0x2762abf6fa22314ebcab41dd4666836038d29341", "Amoy configured PlatformAccountFactory"),
]

for rpc, address, label in checks:
    payload = json.dumps({
        "jsonrpc": "2.0",
        "id": 1,
        "method": "eth_getCode",
        "params": [address, "latest"],
    })
    raw = subprocess.check_output([
        "curl", "-ksS", "--max-time", "20", rpc,
        "-H", "content-type: application/json",
        "--data", payload,
    ], text=True)
    code = json.loads(raw)["result"].lower()
    print(f"{label}: bytes={(len(code)-2)//2} sha256={hashlib.sha256(code.encode()).hexdigest()}")
    if "PlatformAccountFactory" in label:
        print(f"{label}: exact_artifact_match={code == expected}")
PY

Repository: TrustVC/7702SmartAccount

Length of output: 994


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- artifact identity and immutable references ---'
python3 - <<'PY'
import json
from pathlib import Path
p = Path("ignition/deployments/chain-11155111/artifacts/FactoryModule#PlatformAccountFactory.json")
a = json.loads(p.read_text())
for key in ("contractName", "sourceName", "linkReferences", "deployedLinkReferences", "immutableReferences"):
    print(f"{key}: {a.get(key)}")
for key in ("bytecode", "deployedBytecode"):
    v = a[key]
    print(f"{key}: prefix={v[:100]} suffix={v[-100:]}")
PY

printf '%s\n' '--- deployment scripts and source references ---'
sed -n '1,220p' scripts/deployFactory.ts
sed -n '1,220p' scripts/deployImplementation.ts
rg -n 'PlatformAccountFactory|PaymasterImplementation|contractAddress|deploy' scripts src README.md \
  --glob '*.ts' --glob '*.md' | head -200

printf '%s\n' '--- concise runtime prefixes ---'
python3 - <<'PY'
import json, subprocess
checks = [
 ("https://ethereum-sepolia-rpc.publicnode.com","0x1fe801f6af6e9a6c76431db08b121a7de70bc895","Sepolia configured factory"),
 ("https://ethereum-sepolia-rpc.publicnode.com","0x5dcDf7fA6Ab8323F67FD66E89b6CeD4564f9F4Ff","Sepolia metadata factory"),
 ("https://polygon-amoy-bor-rpc.publicnode.com","0x2762abf6fa22314ebcab41dd4666836038d29341","Amoy configured factory"),
]
for rpc, addr, label in checks:
    payload=json.dumps({"jsonrpc":"2.0","id":1,"method":"eth_getCode","params":[addr,"latest"]})
    out=subprocess.check_output(["curl","-ksS","--max-time","20",rpc,"-H","content-type: application/json","--data",payload],text=True)
    code=json.loads(out)["result"]
    print(f"{label}: prefix={code[:100]} suffix={code[-100:]}")
PY

Repository: TrustVC/7702SmartAccount

Length of output: 18768


Correct the PlatformAccountFactory addresses.

Both configured factory addresses contain bytecode, but their 2,934-byte runtime does not match the repository’s contracts/Factory.sol artifact, which has a 16,330-byte runtime. The Sepolia deployment metadata records a different address. Update the constants or publish matching artifacts and ABI.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/constants/index.ts` around lines 8 - 16, Update the ChainId.Sepolia and
ChainId.Amoy entries in PlatformAccountFactory to addresses whose deployed
runtime matches the repository’s contracts/Factory.sol artifact and ABI. Use the
documented Sepolia deployment metadata and corresponding matching deployment for
Amoy; do not retain the current mismatched factory addresses.

49 changes: 48 additions & 1 deletion test/PlatformPaymaster.ts
Original file line number Diff line number Diff line change
Expand Up @@ -317,8 +317,9 @@ describe("PlatformPaymaster", function () {
const { paymaster, paymasterAsOther, mockRegistry, other, user } =
await loadFixture(deployFixture);

// Authorize the registry
// Authorize the registry and whitelist the caller
await paymaster.write.addRegistry([mockRegistry.address]);
await paymaster.write.setUserWhitelist([other.account.address, 1n]);

await paymasterAsOther.write.mintDocument([
mockRegistry.address,
Expand Down Expand Up @@ -356,6 +357,7 @@ describe("PlatformPaymaster", function () {
const { paymaster, paymasterAsOther, mockRegistry, other, user } =
await loadFixture(deployFixture);
await paymaster.write.addRegistry([mockRegistry.address]);
await paymaster.write.setUserWhitelist([other.account.address, 1n]);

await paymasterAsOther.write.mintDocument([
mockRegistry.address, user.account.address, other.account.address, 1n, "0x" as `0x${string}`,
Expand All @@ -366,6 +368,51 @@ describe("PlatformPaymaster", function () {

expect(await paymaster.read.documentsMinted([other.account.address])).to.equal(2n);
});

it("reverts for an unauthorized caller (not whitelisted, not authorizedCaller, not owner)", async function () {
const { paymasterAsOther, mockRegistry, paymaster, other, user } =
await loadFixture(deployFixture);
await paymaster.write.addRegistry([mockRegistry.address]);

await expect(
paymasterAsOther.write.mintDocument([
mockRegistry.address,
user.account.address,
other.account.address,
1n,
"0x" as `0x${string}`,
]),
).to.be.rejectedWith("caller not authorized");
});

it("allows the owner to mint without being whitelisted", async function () {
const { paymaster, mockRegistry, platform, user, other } = await loadFixture(deployFixture);
await paymaster.write.addRegistry([mockRegistry.address]);

// `paymaster` is connected as `platform`, the clone's owner (see deployFixture)
await paymaster.write.mintDocument([
mockRegistry.address,
user.account.address,
other.account.address,
1n,
"0x" as `0x${string}`,
]);

expect(await paymaster.read.documentsMinted([platform.account.address])).to.equal(1n);
});

it("allows an already-authorizedCaller to mint without whitelist credits", async function () {
const { paymaster, paymasterAsOther, mockRegistry, other, user } =
await loadFixture(deployFixture);
await paymaster.write.addRegistry([mockRegistry.address]);
await paymaster.write.addAuthorizedCaller([other.account.address]);

await paymasterAsOther.write.mintDocument([
mockRegistry.address, user.account.address, other.account.address, 1n, "0x" as `0x${string}`,
]);

expect(await paymaster.read.documentsMinted([other.account.address])).to.equal(1n);
});
});

// ─── getUserDailySpend ────────────────────────────────────────────────────
Expand Down
Loading