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
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
8 changes: 0 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 @@ -27,13 +26,6 @@ contract PlatformAccountFactory is Ownable {
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"),
};
}
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;
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