Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add CSMSettleElStealingPenalty motion #271

Merged
merged 5 commits into from
Sep 23, 2024
Merged
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
2,107 changes: 2,107 additions & 0 deletions abi/CSMRegistry.abi.json

Large diffs are not rendered by default.

51 changes: 51 additions & 0 deletions abi/CSMSettleElStealingPenalty.abi.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
[
{
"inputs": [
{
"internalType": "address",
"name": "_trustedCaller",
"type": "address"
},
{ "internalType": "address", "name": "_csm", "type": "address" }
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"inputs": [
{ "internalType": "address", "name": "_creator", "type": "address" },
{ "internalType": "bytes", "name": "_evmScriptCallData", "type": "bytes" }
],
"name": "createEVMScript",
"outputs": [{ "internalType": "bytes", "name": "", "type": "bytes" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "csm",
"outputs": [
{ "internalType": "contract ICSModule", "name": "", "type": "address" }
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{ "internalType": "bytes", "name": "_evmScriptCallData", "type": "bytes" }
],
"name": "decodeEVMScriptCallData",
"outputs": [
{ "internalType": "uint256[]", "name": "", "type": "uint256[]" }
],
"stateMutability": "pure",
"type": "function"
},
{
"inputs": [],
"name": "trustedCaller",
"outputs": [{ "internalType": "address", "name": "", "type": "address" }],
"stateMutability": "view",
"type": "function"
}
]
4 changes: 4 additions & 0 deletions modules/blockChain/contractAddresses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,3 +199,7 @@ export const StonksStablesAllowedRecipientsRegistry: ChainAddressMap = {
[CHAINS.Mainnet]: '0x3f0534CCcFb952470775C516DC2eff8396B8A368',
[CHAINS.Holesky]: '0xDd553C1F88EDCFc2033141Cb908eFf9189988A90',
}

export const CSMRegistry: ChainAddressMap = {
[CHAINS.Holesky]: '0x4562c3e63c2e586cd1651b958c22f88135acad4f',
}
10 changes: 10 additions & 0 deletions modules/blockChain/contracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -450,3 +450,13 @@ export const ContractStonksStablesTopUp = createContractHelpers({
factory: TypeChain.TopUpWithLimitsStablesAbi__factory,
address: EvmAddressesByType[MotionType.StonksStablesTopUp],
})

export const ContractCSMSettleElStealingPenalty = createContractHelpers({
factory: TypeChain.CSMSettleElStealingPenaltyAbi__factory,
address: EvmAddressesByType[MotionType.CSMSettleElStealingPenalty],
})

export const ContractCSMRegistry = createContractHelpers({
factory: TypeChain.CSMRegistryAbi__factory,
address: CONTRACT_ADDRESSES.CSMRegistry,
})
2 changes: 2 additions & 0 deletions modules/motions/evmAddresses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,8 @@ export const EvmAddressesByChain: EvmAddresses = {
[MotionType.StonksStethTopUp]: '0x1240775f1857fB8317bD9ba63f4A8A6A78D9af06',
[MotionType.StonksStablesTopUp]:
'0x65A9913467A9793Bb23726d72C99A470bb9294Ad',
[MotionType.CSMSettleElStealingPenalty]:
'0x07696EA8A5b53C3E35d9cce10cc62c6c79C4691D',

// next motion factories are @deprecated
// we are keeping them here to display history data
Expand Down
21 changes: 21 additions & 0 deletions modules/motions/hooks/useCSMNodeOperatorsCount.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { useLidoSWR } from '@lido-sdk/react'
import { ContractCSMRegistry } from 'modules/blockChain/contracts'
import { useWeb3 } from 'modules/blockChain/hooks/useWeb3'

export const useCSMNodeOperatorsCount = () => {
const { chainId } = useWeb3()
const registry = ContractCSMRegistry.useRpc()

return useLidoSWR(
[`swr:useCSMNodeOperatorsCount`, chainId],
async () => {
const count = (await registry.getNodeOperatorsCount()).toNumber()

return count
},
{
revalidateOnFocus: false,
revalidateOnReconnect: false,
},
)
}
2 changes: 2 additions & 0 deletions modules/motions/hooks/useContractEvmScript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ export const EVM_CONTRACTS = {
[MotionType.LegoStablesTopUp]: CONTRACTS.ContractLegoStablesTopUp,
[MotionType.StonksStethTopUp]: CONTRACTS.ContractStonksStethTopUp,
[MotionType.StonksStablesTopUp]: CONTRACTS.ContractStonksStablesTopUp,
[MotionType.CSMSettleElStealingPenalty]:
CONTRACTS.ContractCSMSettleElStealingPenalty,
} as const

export function useContractEvmScript<T extends MotionType | EvmUnrecognized>(
Expand Down
1 change: 1 addition & 0 deletions modules/motions/hooks/useEVMScriptDecoder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export function useEVMScriptDecoder() {
abis.RegistryWithLimitsAbi__factory.abi,
[KEYS.StonksStablesAllowedRecipientsRegistry]:
abis.RegistryWithLimitsAbi__factory.abi,
[KEYS.CSMRegistry]: abis.CSMRegistryAbi__factory.abi,
}),
)
}, `evm-script-decoder-${chainId}`)
Expand Down
2 changes: 2 additions & 0 deletions modules/motions/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ export const MotionTypeForms = {
LegoStablesTopUp: 'LegoStablesTopUp',
StonksStethTopUp: 'StonksStethTopUp',
StonksStablesTopUp: 'StonksStablesTopUp',

CSMSettleElStealingPenalty: 'CSMSettleElStealingPenalty',
} as const
// intentionally
// eslint-disable-next-line @typescript-eslint/no-redeclare
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { CSMSettleElStealingPenaltyAbi } from 'generated'
import { pluralize } from 'modules/shared/utils/pluralize'
import { NestProps } from './types'

// CSMSettleElStealingPenalty
export function DescCSMSettleElStealingPenalty({
callData,
}: NestProps<CSMSettleElStealingPenaltyAbi['decodeEVMScriptCallData']>) {
return (
<>
Settle (confirm) EL Rewards Stealing penalty for the following CSM{' '}
{pluralize(callData.length, 'operator')}:
{callData.map((item, index) => {
const nodeOperatorId = item.toNumber()

return <div key={index}>NO #{nodeOperatorId}</div>
})}
</>
)
}
2 changes: 2 additions & 0 deletions modules/motions/ui/MotionDescription/MotionDescription.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import { DescSDVTNodeOperatorNamesSet } from './DescSDVTNodeOperatorNamesSet'
import { DescSDVTNodeOperatorsAdd } from './DescSDVTNodeOperatorsAdd'
import { DescSDVTNodeOperatorManagersChange } from './DescSDVTNodeOperatorManagersChange'
import { DescNodeOperatorIncreaseLimit } from './DescNodeOperatorLimitIncrease'
import { DescCSMSettleElStealingPenalty } from './DescCSMSettleElStealingPenalty'

type DescWithLimitsProps = NestProps<
TopUpWithLimitsAbi['decodeEVMScriptCallData']
Expand Down Expand Up @@ -298,6 +299,7 @@ const MOTION_DESCRIPTIONS = {
registryType={MotionType.StonksStethTopUp}
/>
),
[MotionType.CSMSettleElStealingPenalty]: DescCSMSettleElStealingPenalty,
} as const

type Props = {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import { utils } from 'ethers'

import { Fragment } from 'react'
import { useFieldArray, useFormContext } from 'react-hook-form'
import { Plus, ButtonIcon } from '@lidofinance/lido-ui'
import { useWeb3 } from 'modules/blockChain/hooks/useWeb3'

import { PageLoader } from 'modules/shared/ui/Common/PageLoader'
import {
Fieldset,
MessageBox,
RemoveItemButton,
FieldsWrapper,
FieldsHeader,
FieldsHeaderDesc,
} from '../CreateMotionFormStyle'

import { ContractCSMSettleElStealingPenalty } from 'modules/blockChain/contracts'
import { MotionType } from 'modules/motions/types'
import { createMotionFormPart } from './createMotionFormPart'
import { estimateGasFallback } from 'modules/motions/utils'
import { useCSMNodeOperatorsCount } from 'modules/motions/hooks/useCSMNodeOperatorsCount'
import { InputNumberControl } from 'modules/shared/ui/Controls/InputNumber'
import { validateUintValue } from 'modules/motions/utils/validateUintValue'

type NodeOperator = {
id: string
}

export const formParts = createMotionFormPart({
motionType: MotionType.CSMSettleElStealingPenalty,
populateTx: async ({ evmScriptFactory, formData, contract }) => {
const sortedNodeOperators = formData.nodeOperators
.map(({ id }) => Number(id))
.sort((a, b) => a - b)
katamarinaki marked this conversation as resolved.
Show resolved Hide resolved

const encodedCallData = new utils.AbiCoder().encode(
['uint256[]'],
[sortedNodeOperators],
)
const gasLimit = await estimateGasFallback(
contract.estimateGas.createMotion(evmScriptFactory, encodedCallData),
)
const tx = await contract.populateTransaction.createMotion(
evmScriptFactory,
encodedCallData,
{ gasLimit },
)
return tx
},
getDefaultFormData: () => ({
nodeOperators: [
{
id: '',
},
] as NodeOperator[],
}),
Component: ({ fieldNames, submitAction }) => {
const { walletAddress } = useWeb3()

const {
data: nodeOperatorsCount,
initialLoading: isNodeOperatorsCountLoading,
} = useCSMNodeOperatorsCount()

const trustedCaller = ContractCSMSettleElStealingPenalty.useSwrWeb3(
'trustedCaller',
[],
)

const fieldsArr = useFieldArray({ name: fieldNames.nodeOperators })
const { watch } = useFormContext()
const selectedNodeOperators: NodeOperator[] = watch(
fieldNames.nodeOperators,
)

const handleAddSettle = () =>
fieldsArr.append({
id: '',
} as NodeOperator)

if (trustedCaller.initialLoading || isNodeOperatorsCountLoading) {
return <PageLoader />
}

if (trustedCaller.data !== walletAddress) {
return <MessageBox>You should be connected as trusted caller</MessageBox>
}

if (!nodeOperatorsCount) {
return <MessageBox>There are no node operators</MessageBox>
}

return (
<>
{fieldsArr.fields.map((item, fieldIndex) => {
return (
<Fragment key={item.id}>
<FieldsWrapper>
<FieldsHeader>
{fieldsArr.fields.length > 1 && (
<FieldsHeaderDesc>
Settle #{fieldIndex + 1}
</FieldsHeaderDesc>
)}
{fieldsArr.fields.length > 1 && (
<RemoveItemButton
onClick={() => fieldsArr.remove(fieldIndex)}
>
Remove settle {fieldIndex + 1}
</RemoveItemButton>
)}
</FieldsHeader>

<Fieldset>
<InputNumberControl
name={`${fieldNames.nodeOperators}.${fieldIndex}.id`}
label="Node operator ID"
rules={{
required: 'Field is required',
validate: value => {
const uintError = validateUintValue(value)
if (uintError) {
return uintError
}
const valueNum = Number(value)

if (valueNum >= nodeOperatorsCount) {
return 'Invalid node operator ID'
}

const isAlreadyInInput = selectedNodeOperators.some(
({ id }, index) =>
id === value && index !== fieldIndex,
)

if (isAlreadyInInput) {
return 'This ID is already in the list'
}

return true
},
}}
/>
</Fieldset>
</FieldsWrapper>
</Fragment>
)
})}

{selectedNodeOperators.length < nodeOperatorsCount && (
<Fieldset>
<ButtonIcon
type="button"
variant="ghost"
size="sm"
onClick={handleAddSettle}
icon={<Plus />}
color="secondary"
>
One more settle
</ButtonIcon>
</Fieldset>
)}

{submitAction}
</>
)
},
})
3 changes: 3 additions & 0 deletions modules/motions/ui/MotionFormStartNew/Parts/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import * as StartNewSDVTNodeOperatorRewardAddressesSet from './StartNewSDVTNodeO
import * as StartNewSDVTNodeOperatorNamesSet from './StartNewSDVTNodeOperatorNamesSet'
import * as StartNewSDVTNodeOperatorManagersChange from './StartNewSDVTNodeOperatorManagersChange'
import * as StartNewNodeOperatorLimitIncrease from './StartNewNodeOperatorLimitIncrease'
import * as StartNewCSMSettleElStealingPenalty from './StartNewCSMSettleElStealingPenalty'

export const formParts = {
[MotionTypeForms.NodeOperatorIncreaseLimit]:
Expand Down Expand Up @@ -119,6 +120,8 @@ export const formParts = {
StartNewTopUpWithLimitsAndCustomToken.formParts({
registryType: MotionTypeForms.StonksStablesTopUp,
}),
[MotionTypeForms.CSMSettleElStealingPenalty]:
StartNewCSMSettleElStealingPenalty.formParts,
} as const

export type FormData = {
Expand Down
2 changes: 2 additions & 0 deletions modules/motions/utils/getMotionTypeDisplayName.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ export const MotionTypeDisplayNames: Record<
[MotionType.PmlStethTopUp]: 'Top up PML stETH',
[MotionType.AtcStethTopUp]: 'Top up ATC stETH',
[MotionType.LegoStablesTopUp]: 'Top up LEGO stablecoins',
[MotionType.CSMSettleElStealingPenalty]:
'Settle EL Rewards Stealing penalty for CSM operators',

[EvmUnrecognized]: 'Unrecognized evm factory',

Expand Down
2 changes: 2 additions & 0 deletions modules/shared/utils/pluralize.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const pluralize = (count: number, word: string) =>
count === 1 ? word : `${word}s`
Loading