Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
225 changes: 173 additions & 52 deletions manager/src/tasks/system/emergencyCompleteUndelegation.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
import 'dotenv/config'

import { OperationType } from '@safe-global/types-kit'
import { encodeFunctionData, parseAbi, getAddress, encodeAbiParameters } from 'viem/utils'
import { encodeFunctionData, parseAbi, getAddress } from 'viem/utils'
import { createPublicClient, http } from 'viem'
import { mainnet, holesky } from 'viem/chains'
import { readFileSync } from 'fs'
import { apiKit, protocolKitOwnerAdmin } from '../../utils/safe'
import { ADMIN, LIQUID_TOKEN_MANAGER_ADDRESS, DEPLOYMENT, proposeSafeTransaction } from '../../utils/forge'
import {
ADMIN,
EMERGENCY_RESCUE_ADDRESS,
DELEGATION_MANAGER_ADDRESS,
STAKER_NODE_COORDINATOR_ADDRESS,
DEPLOYMENT,
proposeSafeTransaction
} from '../../utils/forge'

/**
* Creates a proposal for `emergencyCompleteUndelegation` on `LiquidTokenManager`
* Creates a proposal for emergencyCompleteUndelegation on EmergencyRescue
* This completes the EigenLayer withdrawals and transfers funds to the multisig
*
* MUST be called at least 7 days (50,400 blocks) after emergencyUndelegateAllNodes
Expand All @@ -26,12 +33,12 @@ export async function emergencyCompleteUndelegation(
if (!ADMIN) throw new Error('Env vars not set correctly.')

// Load saved withdrawal data
console.log(' Loading withdrawal data from:', withdrawalDataFile)
console.log('Loading withdrawal data from:', withdrawalDataFile)
const savedData = JSON.parse(readFileSync(withdrawalDataFile, 'utf-8'))
const nodeIds = savedData.nodeIds
const withdrawalData = savedData.withdrawalData

console.log(` Processing ${nodeIds.length} nodes...\n`)
console.log(`Processing ${nodeIds.length} nodes...\n`)

// Setup client
const chain = DEPLOYMENT === 'mainnet' ? mainnet : holesky
Expand All @@ -50,7 +57,7 @@ export async function emergencyCompleteUndelegation(
const requiredBlock = BigInt(withdrawal.startBlock) + BigInt(50400)
if (currentBlock < requiredBlock) {
throw new Error(
` Withdrawal delay not met for node ${nodeId}!\n` +
`Withdrawal delay not met for node ${nodeId}!\n` +
` Current block: ${currentBlock}\n` +
` Required block: ${requiredBlock}\n` +
` Blocks remaining: ${requiredBlock - currentBlock}`
Expand All @@ -59,73 +66,180 @@ export async function emergencyCompleteUndelegation(
}
}

console.log(' Withdrawal delay check passed\n')
console.log('Withdrawal delay check passed\n')

// Reconstruct withdrawal structs
console.log(' Reconstructing withdrawal structs...')

const contractAddress = LIQUID_TOKEN_MANAGER_ADDRESS
const reconstructAbi = parseAbi([
'function reconstructWithdrawal(uint256,address[],uint256[],uint256,address,uint256) view returns (tuple(address staker, address delegatedTo, address withdrawer, uint256 nonce, uint32 startBlock, address[] strategies, uint256[] scaledShares), bytes32)'
// Get StakerNode addresses from coordinator
const coordinatorAbi = parseAbi([
'function getNodeById(uint256) view returns (address)'
])

const allWithdrawals: any[] = []
const allAssets: any[] = []
// Fetch withdrawals from EigenLayer using saved withdrawalRoots
console.log('Fetching withdrawals from EigenLayer...')

const delegationManagerAbi = parseAbi([
'function getQueuedWithdrawal(bytes32) view returns (tuple(address staker, address delegatedTo, address withdrawer, uint256 nonce, uint32 startBlock, address[] strategies, uint256[] scaledShares), uint256[])'
])

const strategyAbi = parseAbi([
'function underlyingToken() view returns (address)'
])

const allWithdrawals: any[][] = []
const allAssets: any[][] = []

for (const nodeId of nodeIds) {
console.log(`\nProcessing node ${nodeId}...`)

const nodeWithdrawals: any[] = []
const nodeAssets: any[] = []
const nodeAssets: any[][] = []

const withdrawals = withdrawalData[nodeId]
// Get node address from coordinator
const nodeAddress = await client.readContract({
address: STAKER_NODE_COORDINATOR_ADDRESS as `0x${string}`,
abi: coordinatorAbi,
functionName: 'getNodeById',
args: [BigInt(nodeId)]
})

for (const withdrawal of withdrawals) {
// Reconstruct the withdrawal struct
const [reconstructedWithdrawal, withdrawalRoot] = await client.readContract({
address: contractAddress as `0x${string}`,
abi: reconstructAbi,
functionName: 'reconstructWithdrawal',
args: [
BigInt(nodeId),
withdrawal.strategies,
withdrawal.depositShares.map((s: string) => BigInt(s)),
BigInt(withdrawal.nonce),
withdrawal.operator,
BigInt(withdrawal.startBlock)
]
})

nodeWithdrawals.push(reconstructedWithdrawal)
nodeAssets.push(withdrawal.tokens)

console.log(` Node ${nodeId}: Reconstructed withdrawal`)
console.log(` - Root: ${withdrawalRoot}`)
console.log(` - Strategies: ${withdrawal.strategies.length}`)
console.log(` Node address: ${nodeAddress}`)

const savedWithdrawals = withdrawalData[nodeId]

for (const savedWithdrawal of savedWithdrawals) {
console.log(` Processing ${savedWithdrawal.withdrawalRoots.length} withdrawal root(s)...`)

// Process each withdrawal root
for (const withdrawalRoot of savedWithdrawal.withdrawalRoots) {
console.log(` Querying root: ${withdrawalRoot}`)

// Query EigenLayer for the actual withdrawal struct
const [withdrawal, shares] = await client.readContract({
address: DELEGATION_MANAGER_ADDRESS as `0x${string}`,
abi: delegationManagerAbi,
functionName: 'getQueuedWithdrawal',
args: [withdrawalRoot as `0x${string}`]
}) as any

console.log(` Found withdrawal with ${withdrawal.strategies.length} strategies`)

// Validate withdrawal matches our saved data
if (withdrawal.staker.toLowerCase() !== (nodeAddress as string).toLowerCase()) {
throw new Error(
`Withdrawal staker mismatch!\n` +
` Expected: ${nodeAddress}\n` +
` Got: ${withdrawal.staker}`
)
}

if (BigInt(withdrawal.nonce) !== BigInt(savedWithdrawal.nonce)) {
throw new Error(
`Withdrawal nonce mismatch!\n` +
` Expected: ${savedWithdrawal.nonce}\n` +
` Got: ${withdrawal.nonce}`
)
}

if (withdrawal.delegatedTo.toLowerCase() !== savedWithdrawal.operator.toLowerCase()) {
throw new Error(
`Withdrawal operator mismatch!\n` +
` Expected: ${savedWithdrawal.operator}\n` +
` Got: ${withdrawal.delegatedTo}`
)
}

// Add validated withdrawal
nodeWithdrawals.push({
staker: withdrawal.staker,
delegatedTo: withdrawal.delegatedTo,
withdrawer: withdrawal.withdrawer,
nonce: withdrawal.nonce,
startBlock: withdrawal.startBlock,
strategies: withdrawal.strategies,
scaledShares: withdrawal.scaledShares
})

// Get token addresses from strategies
const tokens: string[] = []
console.log(` Fetching tokens from strategies...`)

for (let i = 0; i < withdrawal.strategies.length; i++) {
const strategy = withdrawal.strategies[i]

try {
const token = await client.readContract({
address: strategy as `0x${string}`,
abi: strategyAbi,
functionName: 'underlyingToken'
})
tokens.push(token as string)
console.log(` Strategy ${i}: ${strategy} -> Token: ${token}`)
} catch (error) {
throw new Error(
`Failed to get underlying token for strategy ${strategy}:\n` +
` ${error}`
)
}
}

nodeAssets.push(tokens)
}
}

allWithdrawals.push(nodeWithdrawals)
allAssets.push(nodeAssets)

console.log(` Prepared ${nodeWithdrawals.length} withdrawal(s) for node ${nodeId}`)
}

console.log(' Withdrawal structs reconstructed\n')
console.log('\nAll withdrawal structs and assets prepared\n')

// Setup task params
// Validate data structure before encoding
console.log('Validating data structure...')
console.log(` Node IDs: ${nodeIds.length}`)
console.log(` Withdrawal arrays: ${allWithdrawals.length}`)
console.log(` Asset arrays: ${allAssets.length}`)

if (allWithdrawals.length !== nodeIds.length) {
throw new Error('Withdrawal arrays length mismatch with node IDs')
}

if (allAssets.length !== nodeIds.length) {
throw new Error('Asset arrays length mismatch with node IDs')
}

for (let i = 0; i < nodeIds.length; i++) {
if (allWithdrawals[i].length !== allAssets[i].length) {
throw new Error(
`Withdrawal/Asset count mismatch for node ${nodeIds[i]}:\n` +
` Withdrawals: ${allWithdrawals[i].length}\n` +
` Assets: ${allAssets[i].length}`
)
}
}

console.log('Data structure validation passed\n')

// Setup EmergencyRescue contract call
const contractAddress = EMERGENCY_RESCUE_ADDRESS
const abi = parseAbi([
'function emergencyCompleteUndelegation(uint256[],tuple(address,address,address,uint256,uint32,address[],uint256[])[],address[][][],address)'
'function emergencyCompleteUndelegation(uint256[] calldata nodeIds, tuple(address staker, address delegatedTo, address withdrawer, uint256 nonce, uint32 startBlock, address[] strategies, uint256[] scaledShares)[][] calldata withdrawals, address[][][] calldata assets, address recipient) external'
])

const metadata = {
title: 'Emergency Complete Undelegation',
description: `EMERGENCY: Proposal to complete undelegation and recover funds to ${recipientAddress}. This is step 2 of the emergency recovery process. Funds will be transferred to the specified address.`
description: `EMERGENCY: Proposal to complete undelegation and recover funds to ${recipientAddress}. This is step 2 of the emergency recovery process. Funds will be transferred to the specified address after EigenLayer withdrawal completion.`
}

// Setup transaction data
// Encode function data
console.log('Encoding transaction data...')
const data = encodeFunctionData({
abi,
functionName: 'emergencyCompleteUndelegation',
args: [
nodeIds.map((id: number) => BigInt(id)),
allWithdrawals as any,
allAssets as any,
recipientAddress
getAddress(recipientAddress)
]
})

Expand All @@ -136,29 +250,36 @@ export async function emergencyCompleteUndelegation(
operation: OperationType.Call
}

// Create transaction
// Create Safe transaction
console.log('Creating Safe transaction...')
const nonce = Number(await apiKit.getNextNonce(ADMIN))
const safeTransaction = await protocolKitOwnerAdmin.createTransaction({
transactions: [metaTransactionData],
options: { nonce }
})

// Propose transactions to multisig
// Propose to multisig
console.log('Proposing to multisig...')
await proposeSafeTransaction(safeTransaction, metadata)

console.log(' Emergency completion proposal created successfully')
console.log(` Funds will be sent to: ${recipientAddress}`)
console.log(' After execution, funds can be transferred to the original owner from Safe UI')
console.log('\nEmergency completion proposal created successfully')
console.log(`Recipient: ${recipientAddress}`)
console.log('\nNext steps:')
console.log(' 1. Multisig members review and sign the proposal')
console.log(' 2. Execute the proposal')
console.log(' 3. Funds will be transferred to the recipient address')
console.log(' 4. From Safe UI, transfer funds to the original owner if needed')

} catch (error) {
console.log('Error: ', error)
return []
throw error
}
}

// CLI usage
if (require.main === module) {
const args = process.argv.slice(2)

if (args.length !== 2) {
console.error('Usage: ts-node emergencyCompleteUndelegation.ts <withdrawal-data-file> <recipient-address>')
console.error('Example: ts-node emergencyCompleteUndelegation.ts ./data/emergency-withdrawal-data-2024-01-15.json 0x1234...')
Expand Down
6 changes: 3 additions & 3 deletions manager/src/tasks/system/emergencyUndelegateAllNodes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ import 'dotenv/config'
import { OperationType } from '@safe-global/types-kit'
import { encodeFunctionData, parseAbi, getAddress } from 'viem/utils'
import { apiKit, protocolKitOwnerAdmin } from '../../utils/safe'
import { ADMIN, LIQUID_TOKEN_MANAGER_ADDRESS, proposeSafeTransaction } from '../../utils/forge'
import { ADMIN, EMERGENCY_RESCUE_ADDRESS, proposeSafeTransaction } from '../../utils/forge'

/**
* Creates a proposal for `emergencyUndelegateAllNodes` on `LiquidTokenManager`
* Creates a proposal for `emergencyUndelegateAllNodes` on `EmergencyRescue`
* This will undelegate all nodes and queue withdrawals on EigenLayer
*
* IMPORTANT: After execution, save the returned nodeIds and withdrawalRoots
Expand All @@ -19,7 +19,7 @@ export async function emergencyUndelegateAllNodes() {
if (!ADMIN) throw new Error('Env vars not set correctly.')

// Setup task params
const contractAddress = LIQUID_TOKEN_MANAGER_ADDRESS
const contractAddress = EMERGENCY_RESCUE_ADDRESS // CHANGED since we changed our approach now
const abi = parseAbi(['function emergencyUndelegateAllNodes() returns (uint256[], bytes32[][])'])
const metadata = {
title: 'Emergency Undelegate All Nodes',
Expand Down
Loading