Skip to content

Instantly share code, notes, and snippets.

@Strykar
Created September 2, 2026 23:20
Show Gist options
  • Select an option

  • Save Strykar/1ddef28a20db637677dc05bdbb4670a7 to your computer and use it in GitHub Desktop.

Select an option

Save Strykar/1ddef28a20db637677dc05bdbb4670a7 to your computer and use it in GitHub Desktop.
Verify LIP-118 delegated reward calling against deployed Arbitrum One contracts on a local anvil fork (no mainnet txs)
=== orchestrator under test (first in pool): 0x525419FF5707190389bfb5C87c375D710F5fCb0E ===
hints: prev=0x0000000000000000000000000000000000000000 next=0xac2e50c8f7Ac0f82923a7Df9d9903f6ec4741919
=== 0. baseline ===
transcoderToRewardCaller = 0x0000000000000000000000000000000000000000
RoundsManager.currentRound() = 4323
getTranscoder(orch).lastRewardRound = 4323
=== 1. negative control: selector that does not exist ===
execution reverted
=== 2. unauthorized caller ===
execution reverted: caller must be a reward caller set by the transcoder
=== 3. setRewardCaller from the orchestrator ===
status: 0x1
RewardCallerSet topic: 0x15932b87747cac36d3c505e86a946c6d3a79d9d7bdb79d81584d32bbe54516c2
transcoder : 0x525419ff5707190389bfb5c87c375d710f5fcb0e
rewardCaller: 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266
expected topic: 0x15932b87747cac36d3c505e86a946c6d3a79d9d7bdb79d81584d32bbe54516c2
mapping now: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
=== 4. authorized caller, round not yet initialized ===
execution reverted: current round is not initialized
=== 5. initializeRound, then reward as the delegated caller ===
initializeRound status: 0x1
reward status: 0x1
Reward event -> transcoder: 0x525419ff5707190389bfb5c87c375d710f5fcb0e
Reward event -> amount : 4362.805959257629 LPT
lastRewardRound: 4323 -> 78847 [7.884e4]
currentRound() now: 78847 [7.884e4]
msg.sender: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
=== 6. revoke ===
status: 0x1
mapping now: 0x0000000000000000000000000000000000000000
=== 7. previously-authorized caller, after revoke ===
execution reverted: caller must be a reward caller set by the transcoder
=== END OF TRANSCRIPT (verify-lip118-fork.sh) ===
#!/usr/bin/env bash
# LIP-118 delegation against the deployed Arbitrum One contracts, on a local
# anvil fork. Sends no mainnet transactions.
#
# Uses rewardForTranscoderWithHint (0xb556966a) throughout, not the sibling
# rewardForTranscoder (0x1a8c41f8), because only the former is on the node's
# path. Testing the sibling would pass while proving nothing.
#
# Requires foundry.
set -euo pipefail
RPC=${RPC:-http://127.0.0.1:8545}
BM=0x35Bcf3c30594191d53231E4FF333E8A770453e40 # BondingManager (Arbitrum One)
RM=0xdd6f56DcC28D3F5f27084381fE8Df634985cc39f # RoundsManager (Arbitrum One)
CALLER=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 # anvil account 0
STRANGER=0x70997970C51812dc3A010C7d01b50e0d17dc79C8 # anvil account 1
say() { printf '\n=== %s ===\n' "$*"; }
reason() { grep -o 'execution reverted[^,]*' | head -1 || echo '(no revert)'; }
ORCH=$(cast call --rpc-url "$RPC" $BM "getFirstTranscoderInPool()(address)")
say "orchestrator under test (first in pool): $ORCH"
# Real hints, not a fallback: this orchestrator is first in the pool and reward
# only raises its stake, so it stays first. The node derives these from
# simulateTranscoderPoolUpdate, which is not replicated here.
ZERO=0x0000000000000000000000000000000000000000
PREV=$ZERO
NEXT=$(cast call --rpc-url "$RPC" $BM "getNextTranscoderInPool(address)(address)" "$ORCH")
echo "hints: prev=$PREV next=$NEXT"
say "0. baseline"
echo "transcoderToRewardCaller = $(cast call --rpc-url "$RPC" $BM 'transcoderToRewardCaller(address)(address)' "$ORCH")"
echo "RoundsManager.currentRound() = $(cast call --rpc-url "$RPC" $RM 'currentRound()(uint256)')"
echo "getTranscoder(orch).lastRewardRound = $(cast call --rpc-url "$RPC" $BM 'getTranscoder(address)(uint256,uint256,uint256,uint256,uint256,uint256,uint256)' "$ORCH" | head -1)"
say "1. negative control: selector that does not exist"
{ cast call --rpc-url "$RPC" --from $STRANGER $BM "rewardForNobody(address)" "$ORCH" 2>&1 || true; } | reason
say "2. unauthorized caller"
{ cast call --rpc-url "$RPC" --from $STRANGER $BM "rewardForTranscoderWithHint(address,address,address)" "$ORCH" $ZERO $ZERO 2>&1 || true; } | reason
cast rpc --rpc-url "$RPC" anvil_impersonateAccount "$ORCH" >/dev/null
cast rpc --rpc-url "$RPC" anvil_setBalance "$ORCH" 0xde0b6b3a7640000 >/dev/null
cast rpc --rpc-url "$RPC" anvil_setBalance $CALLER 0xde0b6b3a7640000 >/dev/null
say "3. setRewardCaller from the orchestrator"
cast send --rpc-url "$RPC" --unlocked --from "$ORCH" $BM "setRewardCaller(address)" $CALLER --json \
| python3 -c 'import json,sys; r=json.load(sys.stdin); print("status:",r["status"]); print("RewardCallerSet topic:",r["logs"][0]["topics"][0]); print(" transcoder :","0x"+r["logs"][0]["topics"][1][-40:]); print(" rewardCaller:","0x"+r["logs"][0]["topics"][2][-40:])'
echo "expected topic: $(cast keccak 'RewardCallerSet(address,address)')"
echo "mapping now: $(cast call --rpc-url "$RPC" $BM 'transcoderToRewardCaller(address)(address)' "$ORCH")"
say "4. authorized caller, round not yet initialized"
{ cast call --rpc-url "$RPC" --from $CALLER $BM "rewardForTranscoderWithHint(address,address,address)" "$ORCH" $PREV $NEXT 2>&1 || true; } | reason
say "5. initializeRound, then reward as the delegated caller"
cast send --rpc-url "$RPC" --unlocked --from $CALLER $RM "initializeRound()" --json \
| python3 -c 'import json,sys; print("initializeRound status:",json.load(sys.stdin)["status"])'
BEFORE=$(cast call --rpc-url "$RPC" $BM "getTranscoder(address)(uint256,uint256,uint256,uint256,uint256,uint256,uint256)" "$ORCH" | head -1)
cast send --rpc-url "$RPC" --unlocked --from $CALLER $BM "rewardForTranscoderWithHint(address,address,address)" "$ORCH" $PREV $NEXT --json \
| python3 -c '
import json,sys
r=json.load(sys.stdin); print("reward status:",r["status"])
for l in r["logs"]:
if l["topics"][0]=="0x619caafabdd75649b302ba8419e48cccf64f37f1983ac4727cfb38b57703ffc9":
print("Reward event -> transcoder:","0x"+l["topics"][1][-40:])
print("Reward event -> amount :",int(l["data"],16)/1e18,"LPT")'
AFTER=$(cast call --rpc-url "$RPC" $BM "getTranscoder(address)(uint256,uint256,uint256,uint256,uint256,uint256,uint256)" "$ORCH" | head -1)
echo "lastRewardRound: $BEFORE -> $AFTER"
echo "currentRound() now: $(cast call --rpc-url "$RPC" $RM 'currentRound()(uint256)')"
echo "msg.sender: $CALLER"
say "6. revoke"
cast send --rpc-url "$RPC" --unlocked --from "$ORCH" $BM "setRewardCaller(address)" \
0x0000000000000000000000000000000000000000 --json \
| python3 -c 'import json,sys; print("status:",json.load(sys.stdin)["status"])'
echo "mapping now: $(cast call --rpc-url "$RPC" $BM 'transcoderToRewardCaller(address)(address)' "$ORCH")"
say "7. previously-authorized caller, after revoke"
{ cast call --rpc-url "$RPC" --from $CALLER $BM "rewardForTranscoderWithHint(address,address,address)" "$ORCH" $PREV $NEXT 2>&1 || true; } | reason
echo
echo "=== END OF TRANSCRIPT (verify-lip118-fork.sh) ==="
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment