Skip to content
PortunaPortunaPortunaDocsTestnet

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.

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 0x4e59b44847b379578588920cA78FbF26c0B4956C with 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.

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:

Terminal window
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>
chain 11155111
factory 0x66f63A7246dF0eD76098dD95ad0F53d8e7bed456
batcher 0xD501D131204CaDD7EF8ec07a2d15FfAc05770a63 of gas wallet 0xf0259b04f5D336E624A84780d1B365fAFc3f5A85
delegate 0xE60fdf70a794C76094f86b990a821D55e3096AA5 of hot wallet 0x2Cd50979a8A33e8CA37DA85F8517fb148a67b769
PASS factory code 3671 bytes at 0x66f63A7246dF0eD76098dD95ad0F53d8e7bed456
PASS batcher code 1195 bytes at 0xD501D131204CaDD7EF8ec07a2d15FfAc05770a63
PASS delegate code 955 bytes at 0xE60fdf70a794C76094f86b990a821D55e3096AA5
PASS 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.
FlagMeaning
-rpcAn RPC of the chain; defaults to $RPC_URL. The output names it by host only, since providers put keys in the path.
-gas-walletYour gas wallet: gas_wallet in GET /v1/balance.
-hot-walletThe hot wallet.
-expect-delegateThe delegate the API returned: compared with the computed one.
-factoryA published factory address: compared with the computed one, and used instead of it.
-depositA 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.

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")
}

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:

  1. Compute the factory’s address and compare it with the published one:

    Terminal window
    SALT=0x0000000000000000000000000000000000000000000000000000000000000000
    cast compute-address 0x4e59b44847b379578588920cA78FbF26c0B4956C --salt $SALT \
    --init-code-hash $(cast keccak $(jq -r .bytecode.object out/SweepFactory.sol/SweepFactory.json))
  2. Compute your batcher and the delegate (GAS is your gas wallet, HOT the 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 delegate from POST /v1/hot-wallets.

  3. Check that there is code at these addresses, and, after a first sweep, the code of a deposit address: the delegation marker 0xef0100 followed 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.