Verifying the delegate
Your deposit addresses sign for the delegate address the API returns. Before the first of them does, check that
the contract at that address is the open contract code, built for your hot wallet and your gas wallet. You do not
need to trust Portuna or a block explorer for that.
Why the address proves the code
Section titled “Why the address proves the code”All the contracts are created with CREATE2. Such an address is a hash of the creator’s address, a salt and the
full creation code: the bytecode with its constructor arguments. Change one byte of the code, or the hot wallet in
it, and the address changes. So:
- the factory’s address follows from its bytecode and the standard deployer
0x4e59b44847b379578588920cA78FbF26c0B4956Cwith a zero salt: it is the same on every chain; - your batcher’s address follows from the factory, the batcher’s bytecode and your gas wallet;
- a delegate’s address follows from the factory, the delegate’s bytecode, the hot wallet and your batcher.
If the address you compute from the code is the delegate from the API and holds code, the delegate is that code,
paying only your hot wallet and obeying only your batcher.
With verify-delegate
Section titled “With verify-delegate”The SDKs come with a verify-delegate command. It computes the factory, your batcher and the delegate from the
contract bytecode built into the SDK, checks that each has code on chain, compares the delegate with the one from the
API and, with -deposit, checks the code of deposit addresses after their first sweep.
From the Go SDK’s directory:
go run ./cmd/verify-delegate -rpc https://ethereum-sepolia-rpc.publicnode.com \ -gas-wallet <gas_wallet from GET /v1/balance> \ -hot-wallet <your hot wallet> \ -expect-delegate <delegate from POST /v1/hot-wallets>From the TypeScript SDK’s directory, after npm ci:
npx verify-delegate --rpc https://ethereum-sepolia-rpc.publicnode.com \ --gas-wallet <gas_wallet from GET /v1/balance> \ --hot-wallet <your hot wallet> \ --expect-delegate <delegate from POST /v1/hot-wallets>chain 11155111factory 0x66f63A7246dF0eD76098dD95ad0F53d8e7bed456batcher 0xD501D131204CaDD7EF8ec07a2d15FfAc05770a63 of gas wallet 0xf0259b04f5D336E624A84780d1B365fAFc3f5A85delegate 0xE60fdf70a794C76094f86b990a821D55e3096AA5 of hot wallet 0x2Cd50979a8A33e8CA37DA85F8517fb148a67b769
PASS factory code 3671 bytes at 0x66f63A7246dF0eD76098dD95ad0F53d8e7bed456PASS batcher code 1195 bytes at 0xD501D131204CaDD7EF8ec07a2d15FfAc05770a63PASS delegate code 955 bytes at 0xE60fdf70a794C76094f86b990a821D55e3096AA5PASS delegate from the API 0xE60fdf70a794C76094f86b990a821D55e3096AA5 from the API is the computed one
PASS: the delegate 0xE60fdf70a794C76094f86b990a821D55e3096AA5 is built from the open code for hot wallet 0x2Cd50979a8A33e8CA37DA85F8517fb148a67b769 and gas wallet 0xf0259b04f5D336E624A84780d1B365fAFc3f5A85.| Flag | Meaning |
|---|---|
-rpc | An RPC of the chain; defaults to $RPC_URL. The output names it by host only, since providers put keys in the path. |
-gas-wallet | Your gas wallet: gas_wallet in GET /v1/balance. |
-hot-wallet | The hot wallet. |
-expect-delegate | The delegate the API returned: compared with the computed one. |
-factory | A published factory address: compared with the computed one, and used instead of it. |
-deposit | A deposit address that must already be delegated to the delegate; repeat it for more. |
Both commands take the same flags, with one dash or two. The exit code is 0 if every check passed, 1 if one failed and 2 on wrong arguments, so the command fits in your CI or your hot wallet onboarding script.
From your code
Section titled “From your code”The same checks as a function, VerifyDeployment (verifyDeployment):
eth, err := ethclient.DialContext(ctx, os.Getenv("RPC_URL"))if err != nil { log.Fatal(err)}report, err := portuna.VerifyDeployment(ctx, eth, portuna.Deployment{ GasWallet: common.HexToAddress(os.Getenv("GAS_WALLET")), HotWallet: common.HexToAddress(os.Getenv("HOT_WALLET")), ExpectDelegate: common.HexToAddress(os.Getenv("DELEGATE")),})if err != nil { log.Fatal(err)}for _, c := range report.Checks { fmt.Println(c.OK, c.Name, c.Detail)}if !report.OK() { log.Fatal("do not sign authorizations for this delegate")}const chain = createPublicClient({ transport: http(process.env.RPC_URL) })const report = await verifyDeployment(chain, { gasWallet: process.env.GAS_WALLET as Address, hotWallet: process.env.HOT_WALLET as Address, expectDelegate: process.env.DELEGATE as Address,})for (const c of report.checks) console.log(c.ok ? 'PASS' : 'FAIL', c.name, c.detail)if (!report.ok) throw new Error('do not sign authorizations for this delegate')By hand, with Foundry
Section titled “By hand, with Foundry”To check without the SDK, build the contracts from their source with Foundry. The compiler
and its settings are pinned (solc 0.8.33, optimizer 200 runs, EVM prague; see Contracts),
so the same source gives the same bytecode. In the source tree, cd contracts && forge build, then:
-
Compute the factory’s address and compare it with the published one:
Terminal window SALT=0x0000000000000000000000000000000000000000000000000000000000000000cast compute-address 0x4e59b44847b379578588920cA78FbF26c0B4956C --salt $SALT \--init-code-hash $(cast keccak $(jq -r .bytecode.object out/SweepFactory.sol/SweepFactory.json)) -
Compute your batcher and the delegate (
GASis your gas wallet,HOTthe hot wallet):Terminal window FACTORY=<the address from step 1>BATCHER=$(cast compute-address $FACTORY --salt $SALT --init-code \$(jq -r .bytecode.object out/SweepBatcher.sol/SweepBatcher.json)$(cast abi-encode 'f(address)' $GAS | cut -c3-) | awk '{print $3}')cast compute-address $FACTORY --salt $SALT --init-code \$(jq -r .bytecode.object out/SweepDelegate.sol/SweepDelegate.json)$(cast abi-encode 'f(address,address)' $HOT $BATCHER | cut -c3-)The result must be the
delegatefromPOST /v1/hot-wallets. -
Check that there is code at these addresses, and, after a first sweep, the code of a deposit address: the delegation marker
0xef0100followed by the delegate’s address.Terminal window cast codesize <address> --rpc-url <RPC>cast code <deposit address> --rpc-url <RPC>
The compiler appends a hash of the metadata, sources included, to the bytecode: build from the exact source, untouched. Even a changed comment gives another bytecode and another address.