LIP: Delegated Reward Calling Discussion Thread

Following up on my own post above.. so I signed the LIP-118 call on a device. The PKCS#11 work is still in progress, but I hit something first that affects anyone moving their reward caller onto a hardware wallet.

Signing setRewardCaller on a Trezor Model T, current firmware 2.12.4
This is the entire review the device gives you:

Four screens, and the problem is not really any one of them.

What the device shows is a field labelled DATA, a byte count, and 36 bytes of hex. Nothing on it says which protocol this belongs to, which contract, or what the call does. It is a jumble of digits with a length attached.

Even if you accept that and try to check it, you would need the expected calldata already open beside you, computed in advance, and then compare character by character. Nothing in the flow tells you to do that or hands you the string to compare against and nobody wants to read hex to authenticate transaction flows.

Everything else makes that worse rather than causing it. The bytes are paged, so the 20-byte address lands 22 characters on one screen and 18 on the next. CONFIRM ALL is offered on the first page, so all 36 bytes can be approved having seen 27. The contract being called is not shown until panel 3, after the data is approved. And the string setRewardCaller appears nowhere, so nothing on screen says this grants another key the power to call reward for you.

Put the whole payload on one screen and it is still unverifiable in practice. The fix is not more screen, it is showing the operator what the call means. This is one device. If you run a Ledger or a Safe, worth posting what yours shows.

None of this is a fault in LIP-118; the contract does what the LIP says. The device side was never part of the picture, and cold custody is where it decides whether the feature is usable.

It bears on @ydiaz’s rotation question in post 14. Rotation is cheap on chain, but each one is another approval where the only review the hardware wallet offers is raw calldata, and someone rotating away from a compromised caller is doing it when misreading an address costs most.

Reproducing it on your own device, without touching your orchestrator key:

trezorctl ethereum sign-tx -n "<an unused path>" \
  -c 31337 -i <nonce for that path> -g 100000 -G 100000000 \
  -d 0x0103e60e000000000000000000000000<the caller address, no 0x> \
  0x35Bcf3c30594191d53231E4FF333E8A770453e40 0

Chain id 31337 rather than 42161 on purpose, so the signature cannot land on Arbitrum One, which is also why the summary panel reads UNKN. setRewardCaller is whenSystemNotPaused only with no transcoder check, so any unused key reproduces it.

The fix. Trezor firmware 2.12.4 already implements ERC-7730 clear signing: it decodes calldata against a published descriptor and shows real values instead of hex. Lido, 1inch and LI.FI have one. Livepeer does not, which is why the device falls back to the hex dump.

I have posted one for BondingManager, nine functions covering reward calling, staking and withdrawal, with test vectors taken from real Arbitrum transactions including the rollout runs from this thread:

A registry entry is necessary but not sufficient. Aave and Tether are both in it and neither is served by Trezor yet, because Trezor builds and signs the published definitions from a source that is not public. Merging is the prerequisite; publication is their call. Nobody’s device is about to change next week.

One prerequisite has landed. Descriptor tooling reads ABIs from Sourcify, and our managers sit behind ManagerProxy, which keeps no implementation pointer in a namespaced slot, so Sourcify answered isProxy: false for all of them. Fixed in shazow/whatsabi#216 and argotorg/sourcify#2959, live on staging and awaiting Sourcify’s next production deploy.

The software wallet side is not fixed either. MetaMask falls back to 4byte.directory, which does carry setRewardCaller, so you get the function name. But Sourcify’s v1 API went off on 7 July and MetaMask still calls it, so named parameters degrade to Param #1 and a bare type. ManagerProxy compounds it: MetaMask locates implementations by reading two well-known storage slots, ours is in neither, so it gets the proxy’s ABI without the forwarded selector.

Three PRs open against MetaMask for that, measured against our own BondingManager:
4byte resolves 49 of its 55 selectors, Sourcify resolves all 55.

None merged yet. Same problem as the descriptor, one layer up and await review.

Until those land, the mitigation follows from the problem: do not try to read the hex, generate it first and have it open while you approve.

cast calldata 'setRewardCaller(address)' <your caller address>

That prints exactly the string the device will page through, so you are comparing two things rather than interpreting one. A software wallet at least names the function, but will not name the parameters for our contracts. @Titan-Node, same applies to anyone pointing their caller at livepeer.bot.

1 Like