Skip to content

Commit 81bf93a

Browse files
feat: add gha multisig (#112)
Co-authored-by: Copilot <[email protected]>
1 parent 7dcf930 commit 81bf93a

File tree

4 files changed

+399
-0
lines changed

4 files changed

+399
-0
lines changed
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
name: Bridge Pause/Unpause via Safe Multisig
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
operation:
7+
description: 'Pause operation to perform'
8+
required: true
9+
type: choice
10+
options:
11+
- pause-bridge
12+
- unpause-bridge
13+
- pause-outbound
14+
- unpause-outbound
15+
network:
16+
description: 'Network to perform operation on'
17+
required: true
18+
type: choice
19+
options:
20+
- ethereum
21+
- arbitrum
22+
- sepolia
23+
- arbitrum_sepolia
24+
default: sepolia
25+
dry-run:
26+
description: 'Dry run mode (only prepare and display transaction, do not propose to Safe)'
27+
required: false
28+
type: boolean
29+
default: true
30+
31+
jobs:
32+
prepare-transaction-calldata:
33+
runs-on: ubuntu-latest
34+
environment: ${{ inputs.network }}
35+
outputs:
36+
transaction-data: ${{ steps.prepare.outputs.transaction-data }}
37+
safe-address: ${{ steps.prepare.outputs.safe-address }}
38+
bridge-address: ${{ steps.prepare.outputs.bridge-address }}
39+
40+
steps:
41+
- name: Checkout repository
42+
uses: actions/checkout@v4
43+
with:
44+
submodules: recursive
45+
46+
- name: Install Foundry
47+
uses: foundry-rs/foundry-toolchain@v1
48+
with:
49+
version: stable
50+
cache: true
51+
52+
- name: Prepare transaction calldata
53+
id: prepare
54+
env:
55+
CHAIN: ${{ inputs.network }}
56+
run: |
57+
# Get bridge address from config
58+
BRIDGE_ADDRESS=$(jq -r ".chains.${CHAIN}.iexecLayerZeroBridgeAddress" config/config.json)
59+
echo "bridge-address=$BRIDGE_ADDRESS" >> $GITHUB_OUTPUT
60+
61+
# Determine the function selector and name based on operation
62+
case "${{ inputs.operation }}" in
63+
"pause-bridge")
64+
TRANSACTION_DATA=$(cast calldata "pause()")
65+
FUNCTION_NAME="pause()"
66+
;;
67+
"unpause-bridge")
68+
TRANSACTION_DATA=$(cast calldata "unpause()")
69+
FUNCTION_NAME="unpause()"
70+
;;
71+
"pause-outbound")
72+
TRANSACTION_DATA=$(cast calldata "pauseOutboundTransfers()")
73+
FUNCTION_NAME="pauseOutboundTransfers()"
74+
;;
75+
"unpause-outbound")
76+
TRANSACTION_DATA=$(cast calldata "unpauseOutboundTransfers()")
77+
FUNCTION_NAME="unpauseOutboundTransfers()"
78+
;;
79+
esac
80+
81+
echo "transaction-data=$TRANSACTION_DATA" >> $GITHUB_OUTPUT
82+
echo "safe-address=${{ vars.SAFE_ADDRESS }}" >> $GITHUB_OUTPUT
83+
84+
# Display transaction details
85+
echo "=========================================="
86+
echo "Transaction Details"
87+
echo "=========================================="
88+
echo "Workflow Configuration:"
89+
echo " • Network: ${{ inputs.network }}"
90+
echo " • Operation: ${{ inputs.operation }}"
91+
echo " • Function: $FUNCTION_NAME"
92+
echo " • Safe Address: ${{ vars.SAFE_ADDRESS }}"
93+
echo " • Dry Run: ${{ inputs.dry-run }}"
94+
echo ""
95+
echo "Transaction Details:"
96+
echo " • Target: $BRIDGE_ADDRESS"
97+
echo " • Value: 0 ETH"
98+
echo " • Data: $TRANSACTION_DATA"
99+
echo ""
100+
101+
if [ "${{ inputs.dry-run }}" == "true" ]; then
102+
echo "✅ DRY RUN MODE: Transaction prepared successfully"
103+
fi
104+
105+
propose-to-safe-tx:
106+
needs: prepare-transaction-calldata
107+
uses: ./.github/workflows/propose-safe-transaction.yml
108+
secrets:
109+
rpc-url: ${{ secrets.RPC_URL }}
110+
safe-proposer-private-key: ${{ secrets.SAFE_PROPOSER_PRIVATE_KEY }}
111+
safe-api-key: ${{ secrets.SAFE_API_KEY }}
112+
with:
113+
network: ${{ inputs.network }}
114+
safe-address: ${{ needs.prepare-transaction-calldata.outputs.safe-address }}
115+
transaction-to: ${{ needs.prepare-transaction-calldata.outputs.bridge-address }}
116+
transaction-data: ${{ needs.prepare-transaction-calldata.outputs.transaction-data }}
117+
dry-run: ${{ inputs.dry-run }}
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
name: Manage Contract Roles via Safe Multisig
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
operation:
7+
description: 'Role operation to perform'
8+
required: true
9+
type: choice
10+
options:
11+
- grant
12+
- revoke
13+
role:
14+
description: 'Role to grant or revoke'
15+
required: true
16+
type: choice
17+
options:
18+
- TOKEN_BRIDGE_ROLE
19+
- PAUSER_ROLE
20+
- UPGRADER_ROLE
21+
default: TOKEN_BRIDGE_ROLE
22+
network:
23+
description: 'Network to perform operation on'
24+
required: true
25+
type: choice
26+
options:
27+
- ethereum
28+
- arbitrum
29+
- sepolia
30+
- arbitrum_sepolia
31+
default: sepolia
32+
target_address:
33+
description: 'Address to grant/revoke role to/from'
34+
required: true
35+
type: string
36+
dry-run:
37+
description: 'Dry run mode (only prepare and display transaction, do not propose to Safe)'
38+
required: false
39+
type: boolean
40+
default: true
41+
42+
jobs:
43+
prepare-transaction-calldata:
44+
runs-on: ubuntu-latest
45+
environment: ${{ inputs.network }}
46+
outputs:
47+
transactions: ${{ steps.prepare.outputs.transactions }}
48+
safe-address: ${{ steps.prepare.outputs.safe-address }}
49+
50+
steps:
51+
- name: Checkout repository
52+
uses: actions/checkout@v4
53+
with:
54+
submodules: recursive
55+
56+
- name: Install Foundry
57+
uses: foundry-rs/foundry-toolchain@v1
58+
with:
59+
version: stable
60+
cache: true
61+
62+
- name: Prepare transaction calldata
63+
id: prepare
64+
env:
65+
CHAIN: ${{ inputs.network }}
66+
ROLE_NAME: ${{ inputs.role }}
67+
run: |
68+
# Determine which contract to use based on role and network config
69+
APPROVAL_REQUIRED=$(jq -r ".chains.${CHAIN}.approvalRequired" config/config.json)
70+
BRIDGE_ADDRESS=$(jq -r ".chains.${CHAIN}.iexecLayerZeroBridgeAddress" config/config.json)
71+
72+
if [ "$APPROVAL_REQUIRED" = "true" ]; then
73+
TOKEN_CONTRACT=$(jq -r ".chains.${CHAIN}.rlcLiquidityUnifierAddress" config/config.json)
74+
else
75+
TOKEN_CONTRACT=$(jq -r ".chains.${CHAIN}.rlcCrosschainTokenAddress" config/config.json)
76+
fi
77+
78+
# Calculate role hash: keccak256("ROLE_NAME")
79+
ROLE_HASH=$(cast keccak "$ROLE_NAME")
80+
81+
# Determine the function selector and encode calldata
82+
case "${{ inputs.operation }}" in
83+
grant)
84+
TRANSACTION_DATA=$(cast calldata "grantRole(bytes32,address)" "$ROLE_HASH" "${{ inputs.target_address }}")
85+
FUNCTION_NAME="grantRole(bytes32,address)"
86+
;;
87+
revoke)
88+
TRANSACTION_DATA=$(cast calldata "revokeRole(bytes32,address)" "$ROLE_HASH" "${{ inputs.target_address }}")
89+
FUNCTION_NAME="revokeRole(bytes32,address)"
90+
;;
91+
*)
92+
echo "❌ Error: Unknown operation ${{ inputs.operation }}"
93+
exit 1
94+
;;
95+
esac
96+
97+
# Prepare transactions array based on role type
98+
TRANSACTIONS='[]'
99+
100+
case "$ROLE_NAME" in
101+
TOKEN_BRIDGE_ROLE)
102+
# TOKEN_BRIDGE_ROLE is only on token contract (RLCLiquidityUnifier or RLCCrosschainToken)
103+
TRANSACTIONS=$(echo "$TRANSACTIONS" | jq -c --arg to "$TOKEN_CONTRACT" --arg data "$TRANSACTION_DATA" \
104+
'. += [{"to": $to, "data": $data, "contract": "token"}]')
105+
;;
106+
PAUSER_ROLE)
107+
# PAUSER_ROLE is only on bridge contract
108+
TRANSACTIONS=$(echo "$TRANSACTIONS" | jq -c --arg to "$BRIDGE_ADDRESS" --arg data "$TRANSACTION_DATA" \
109+
'. += [{"to": $to, "data": $data, "contract": "bridge"}]')
110+
;;
111+
UPGRADER_ROLE)
112+
# UPGRADER_ROLE is on both bridge and token contracts
113+
# Add transaction for token contract
114+
TRANSACTIONS=$(echo "$TRANSACTIONS" | jq -c --arg to "$TOKEN_CONTRACT" --arg data "$TRANSACTION_DATA" \
115+
'. += [{"to": $to, "data": $data, "contract": "token"}]')
116+
# Add transaction for bridge contract
117+
TRANSACTIONS=$(echo "$TRANSACTIONS" | jq -c --arg to "$BRIDGE_ADDRESS" --arg data "$TRANSACTION_DATA" \
118+
'. += [{"to": $to, "data": $data, "contract": "bridge"}]')
119+
;;
120+
*)
121+
echo "❌ Error: Unknown role $ROLE_NAME"
122+
'. += [{"to": $to, "data": $data, "contract": "bridge"}]')
123+
;;
124+
esac
125+
126+
echo "transactions=$(echo $TRANSACTIONS | jq -c .)" >> $GITHUB_OUTPUT
127+
echo "safe-address=${{ vars.SAFE_ADDRESS }}" >> $GITHUB_OUTPUT
128+
129+
# Display transaction details for dry-run or verification
130+
echo "=========================================="
131+
echo "Transaction Details"
132+
echo "=========================================="
133+
echo "Workflow Configuration:"
134+
echo " • Network: ${{ inputs.network }}"
135+
echo " • Role: $ROLE_NAME"
136+
echo " • Operation: ${{ inputs.operation }}"
137+
echo " • Function: $FUNCTION_NAME"
138+
echo " • Safe Address: ${{ vars.SAFE_ADDRESS }}"
139+
echo " • Dry Run: ${{ inputs.dry-run }}"
140+
echo ""
141+
echo "────────────────────────────────────────────────────────────────────────────────"
142+
echo ""
143+
144+
# Display each transaction
145+
TX_COUNT=$(echo $TRANSACTIONS | jq 'length')
146+
for i in $(seq 0 $(($TX_COUNT - 1))); do
147+
TX=$(echo $TRANSACTIONS | jq -r ".[$i]")
148+
TX_TO=$(echo $TX | jq -r '.to')
149+
TX_DATA=$(echo $TX | jq -r '.data')
150+
TX_CONTRACT=$(echo $TX | jq -r '.contract')
151+
152+
echo "Transaction #$((i + 1)) - ${TX_CONTRACT^} Contract:"
153+
echo " • Target: $TX_TO"
154+
echo " • Address: ${{ inputs.target_address }}"
155+
echo " • Role Name: $ROLE_NAME"
156+
echo " • Role Hash: $ROLE_HASH"
157+
echo " • Value: 0 ETH"
158+
echo " • Data: $TX_DATA"
159+
echo ""
160+
done
161+
162+
echo "────────────────────────────────────────────────────────────────────────────────"
163+
echo ""
164+
165+
if [ "${{ inputs.dry-run }}" == "true" ]; then
166+
echo "✅ DRY RUN MODE: Transaction(s) prepared successfully"
167+
echo "ℹ️ These transaction(s) would be proposed to Safe multisig"
168+
echo "ℹ️ Re-run with dry-run=false to actually propose to Safe"
169+
fi
170+
171+
propose-to-safe-tx:
172+
needs: prepare-transaction-calldata
173+
strategy:
174+
matrix:
175+
transaction: ${{ fromJson(needs.prepare-transaction-calldata.outputs.transactions) }}
176+
uses: ./.github/workflows/propose-safe-transaction.yml
177+
secrets:
178+
rpc-url: ${{ secrets.RPC_URL }}
179+
safe-proposer-private-key: ${{ secrets.SAFE_PROPOSER_PRIVATE_KEY }}
180+
safe-api-key: ${{ secrets.SAFE_API_KEY }}
181+
with:
182+
network: ${{ inputs.network }}
183+
safe-address: ${{ needs.prepare-transaction-calldata.outputs.safe-address }}
184+
transaction-to: ${{ matrix.transaction.to }}
185+
transaction-data: ${{ matrix.transaction.data }}
186+
dry-run: ${{ inputs.dry-run }}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
name: Propose Safe Transaction (Reusable)
2+
3+
on:
4+
workflow_call:
5+
inputs:
6+
network:
7+
description: 'Network environment'
8+
required: true
9+
type: string
10+
safe-address:
11+
description: 'Safe multisig address'
12+
required: true
13+
type: string
14+
transaction-to:
15+
description: 'Transaction target address'
16+
required: true
17+
type: string
18+
transaction-data:
19+
description: 'Transaction calldata'
20+
required: true
21+
type: string
22+
transaction-value:
23+
description: 'Transaction value in wei'
24+
required: false
25+
type: string
26+
default: "0"
27+
dry-run:
28+
description: 'Dry run mode'
29+
required: false
30+
type: boolean
31+
default: true
32+
secrets:
33+
rpc-url:
34+
description: 'RPC URL for the network'
35+
required: true
36+
safe-proposer-private-key:
37+
description: 'Private key of the Safe proposer'
38+
required: true
39+
safe-api-key:
40+
description: 'Safe API key'
41+
required: true
42+
43+
jobs:
44+
propose-transaction:
45+
runs-on: ubuntu-latest
46+
environment: ${{ inputs.network }}
47+
steps:
48+
- name: Checkout Safe proposal repository
49+
uses: actions/checkout@v4
50+
with:
51+
repository: iExecBlockchainComputing/github-actions-workflows
52+
path: .github-actions
53+
54+
- name: Setup Node.js
55+
uses: actions/setup-node@v4
56+
with:
57+
node-version: '22'
58+
59+
- name: Install dependencies
60+
working-directory: .github-actions/propose-safe-multisig-tx
61+
run: npm ci
62+
63+
- name: Build the action
64+
working-directory: .github-actions/propose-safe-multisig-tx
65+
run: npm run build
66+
67+
- name: Propose transaction to Safe
68+
working-directory: .github-actions/propose-safe-multisig-tx
69+
env:
70+
SAFE_ADDRESS: ${{ inputs.safe-address }}
71+
TRANSACTION_TO: ${{ inputs.transaction-to }}
72+
TRANSACTION_VALUE: ${{ inputs.transaction-value }}
73+
TRANSACTION_DATA: ${{ inputs.transaction-data }}
74+
RPC_URL: ${{ secrets.rpc-url }}
75+
SAFE_PROPOSER_PRIVATE_KEY: ${{ secrets.safe-proposer-private-key }}
76+
SAFE_API_KEY: ${{ secrets.safe-api-key }}
77+
DRY_RUN: ${{ inputs.dry-run }}
78+
run: npm run propose

0 commit comments

Comments
 (0)