Skip to content

Commit

Permalink
Merge pull request #232 from lidofinance/develop
Browse files Browse the repository at this point in the history
Merge into main from develop
  • Loading branch information
AnnaSila authored Feb 19, 2025
2 parents d54128f + 99c843a commit 7537530
Show file tree
Hide file tree
Showing 19 changed files with 3,300 additions and 23 deletions.
1,375 changes: 1,375 additions & 0 deletions abi/DualGovernance.abi.json

Large diffs are not rendered by default.

1,150 changes: 1,150 additions & 0 deletions abi/EmergencyProtectedTimelock.abi.json

Large diffs are not rendered by default.

126 changes: 126 additions & 0 deletions abi/TimelockedGovernance.abi.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
[
{
"inputs": [
{ "internalType": "address", "name": "governance", "type": "address" },
{
"internalType": "contract ITimelock",
"name": "timelock",
"type": "address"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"inputs": [
{ "internalType": "address", "name": "caller", "type": "address" }
],
"name": "CallerIsNotGovernance",
"type": "error"
},
{
"inputs": [
{ "internalType": "address", "name": "governance", "type": "address" }
],
"name": "InvalidGovernance",
"type": "error"
},
{
"inputs": [
{
"internalType": "contract ITimelock",
"name": "timelock",
"type": "address"
}
],
"name": "InvalidTimelock",
"type": "error"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "proposerAccount",
"type": "address"
},
{
"indexed": true,
"internalType": "uint256",
"name": "proposalId",
"type": "uint256"
},
{
"indexed": false,
"internalType": "string",
"name": "metadata",
"type": "string"
}
],
"name": "ProposalSubmitted",
"type": "event"
},
{
"inputs": [],
"name": "GOVERNANCE",
"outputs": [{ "internalType": "address", "name": "", "type": "address" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "TIMELOCK",
"outputs": [
{ "internalType": "contract ITimelock", "name": "", "type": "address" }
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{ "internalType": "uint256", "name": "proposalId", "type": "uint256" }
],
"name": "canScheduleProposal",
"outputs": [{ "internalType": "bool", "name": "", "type": "bool" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "cancelAllPendingProposals",
"outputs": [{ "internalType": "bool", "name": "", "type": "bool" }],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{ "internalType": "uint256", "name": "proposalId", "type": "uint256" }
],
"name": "scheduleProposal",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"components": [
{ "internalType": "address", "name": "target", "type": "address" },
{ "internalType": "uint96", "name": "value", "type": "uint96" },
{ "internalType": "bytes", "name": "payload", "type": "bytes" }
],
"internalType": "struct ExternalCall[]",
"name": "calls",
"type": "tuple[]"
},
{ "internalType": "string", "name": "metadata", "type": "string" }
],
"name": "submitProposal",
"outputs": [
{ "internalType": "uint256", "name": "proposalId", "type": "uint256" }
],
"stateMutability": "nonpayable",
"type": "function"
}
]
224 changes: 224 additions & 0 deletions modules/blockChain/CalldataDecoder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
import { BigNumber, Contract, utils } from 'ethers'
import { ABI } from './types'
import { getStaticRpcBatchProvider } from '@lido-sdk/providers'
import { CHAINS } from '@lido-sdk/constants'
import { StaticJsonRpcBatchProvider } from '@lido-sdk/providers/dist/esm/staticJsonRpcBatchProvider'
import { getPanicReason } from './utils/getPanicReason'
import * as addressMaps from 'modules/blockChain/contractAddresses'
import { isAddress } from 'ethers/lib/utils'

type ContractName = keyof typeof addressMaps

type FactoryName = `${ContractName}Abi__factory`

const getContractName = (factoryName: FactoryName): ContractName =>
factoryName.split('Abi__factory')[0] as ContractName

type FactoryMap = Record<string, { abi: ABI }>

type FunctionSignatureIndex = Record<
string,
| {
factoryName: FactoryName
abi: ABI
}[]
| undefined
>

type DecodedError = {
reason: string
isCustomError: boolean
errorName?: string
errorArgs?: any
}

type SimulationResult = {
success: boolean
error?: DecodedError
}

type CalldataParam =
| string
| number
| BigNumber
| {
readonly [key: string]: CalldataParam
}[]

export type CalldataParams = {
readonly [key: string]: CalldataParam
}

export type DecodedCalldata = {
factoryName: FactoryName
contractName: ContractName
functionName: string
params: CalldataParams
}
export class CalldataDecoder {
private signatureIndex: FunctionSignatureIndex = {}
private provider: StaticJsonRpcBatchProvider
private chainId: CHAINS
private abiMap: Record<string, ABI> = {}

constructor(factoryMap: FactoryMap, chainId: CHAINS, rpcUrl: string) {
this.buildSignatureIndex(factoryMap)
this.provider = getStaticRpcBatchProvider(chainId, rpcUrl)
this.chainId = chainId
}

public decode(calldata: string): DecodedCalldata[] {
if (!calldata.startsWith('0x')) {
return []
}

// Extract function selector (first 4 bytes after 0x)
const selector = calldata.slice(0, 10)
const potentialMatches = this.signatureIndex[selector]

if (!potentialMatches) {
return []
}

const matches: DecodedCalldata[] = []

// Try each potential match
for (const match of potentialMatches) {
try {
const iface = new utils.Interface(match.abi)
const decoded = iface.parseTransaction({ data: calldata })
matches.push({
factoryName: match.factoryName,
contractName: getContractName(match.factoryName),
functionName: decoded.name,
params: decoded.args,
})
} catch {
continue
}
}

return matches
}

public async simulateTransaction(
matchedCalldata: DecodedCalldata,
from?: string,
): Promise<SimulationResult> {
const contractAddress =
addressMaps[matchedCalldata.contractName][this.chainId]

if (!contractAddress) {
return {
success: false,
error: {
reason: `Contract address not found for ${matchedCalldata.contractName}`,
isCustomError: true,
},
}
}

const abi = this.abiMap[matchedCalldata.factoryName]
const contract = new Contract(contractAddress, abi, this.provider)

try {
if (from?.length && !isAddress(from)) {
throw new Error('Invalid from address')
}

const paramsArray = Array.isArray(matchedCalldata.params)
? matchedCalldata.params
: Object.values(matchedCalldata.params)

await contract.callStatic[matchedCalldata.functionName](...paramsArray, {
from,
})
} catch (error: any) {
console.error('Simulation error:', error)
const errorString: string = error.toString()

if (errorString.includes('errorName=')) {
const errorNameMatch = errorString.match(/errorName="([^"]+)"/)
const errorArgsMatch = errorString.match(/errorArgs=\[(.*?)\]/)

return {
success: false,
error: {
reason: `${errorNameMatch?.[1] || 'Unknown'}`,
errorName: errorNameMatch?.[1],
errorArgs:
errorArgsMatch?.[1]?.split(',').map(arg => arg.trim()) || [],
isCustomError: true,
},
}
}

// Check for regular revert string
const revertMatch = errorString.match(
/reverted with reason string '(.*)'/,
)
if (revertMatch) {
return {
success: false,
error: {
reason: revertMatch[1],
isCustomError: false,
},
}
}

// Check for panic code
const panicMatch = errorString.match(
/reverted with panic code (0x[0-9a-f]+)/,
)
if (panicMatch) {
return {
success: false,
error: {
reason: `Panic: ${getPanicReason(panicMatch[1])}`,
isCustomError: false,
},
}
}

// Default error case
return {
success: false,
error: {
reason: errorString,
isCustomError: false,
},
}
}

return {
success: true,
}
}

private buildSignatureIndex(factoryMap: FactoryMap): void {
for (const [factoryName, factory] of Object.entries(factoryMap)) {
// Only index function entries
const functionAbi = factory.abi.filter(item => item.type === 'function')

for (const func of functionAbi) {
const iface = new utils.Interface([func])
const functionFragment = Object.values(iface.functions)[0]
const signature = iface.getSighash(functionFragment)

if (!this.signatureIndex[signature]) {
this.signatureIndex[signature] = []
}

this.signatureIndex[signature]!.push({
factoryName: factoryName as FactoryName,
abi: [func],
})
}
}

Object.entries(factoryMap).map(([factoryName, factory]) => {
this.abiMap[factoryName] = factory.abi
})
}
}
17 changes: 17 additions & 0 deletions modules/blockChain/contractAddresses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,23 @@ export const CSVerifier: ChainAddressMap = {
[CHAINS.Holesky]: '0x6FDAA094227CF8E1593f9fB9C1b867C1f846F916',
}

export const CSVerifierProposed: ChainAddressMap = {
[CHAINS.Mainnet]: '0x3Dfc50f22aCA652a0a6F28a0F892ab62074b5583', // TODO: update address after deployment
[CHAINS.Holesky]: '0xC099dfD61F6E5420e0Ca7e84D820daAd17Fc1D44',
}

export const SandboxNodeOperatorsRegistry: ChainAddressMap = {
[CHAINS.Holesky]: '0xD6C2ce3BB8bea2832496Ac8b5144819719f343AC',
}

export const TimelockedGovernance: ChainAddressMap = {
[CHAINS.Holesky]: '0x2D99B1Fe6AFA9d102C7125908081414b5C3Cc759',
}

export const EmergencyProtectedTimelock: ChainAddressMap = {
[CHAINS.Holesky]: '0xd70D836D60622D48648AA1dE759361D6B9a4Baa0',
}

export const DualGovernance: ChainAddressMap = {
[CHAINS.Holesky]: '0xb291a7f092d5cce0a3c93ea21bda3431129db202',
}
15 changes: 15 additions & 0 deletions modules/blockChain/hooks/useCalldataDecoder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { useGlobalMemo } from 'modules/shared/hooks/useGlobalMemo'
import { CalldataDecoder } from '../CalldataDecoder'
import * as factories from 'generated/factories'
import { useWeb3 } from 'modules/blockChain/hooks/useWeb3'
import { useConfig } from 'modules/config/hooks/useConfig'

export function useCalldataDecoder(): CalldataDecoder {
const { chainId } = useWeb3()
const { getRpcUrl } = useConfig()

return useGlobalMemo(() => {
const rpcUrl = getRpcUrl(chainId)
return new CalldataDecoder(factories, chainId, rpcUrl)
}, `calldata-decoder`)
}
Loading

0 comments on commit 7537530

Please sign in to comment.