Delegating Tokens

The deposited tokens can now be delegated to your validator.

## Script constants, repeated here for ease of reference
EXO_ETH_ADDR=0x83E6850591425e3C1E263c054f4466838B9Bd9e4
WST_ETH_ADDR=0xB82381A3fBD3FaFA77B3a7bE693342618240067b
GATEWAY_ADDR=0xDf9caDCfb027d9f6264Ecd5eAEc839a8335d8520

## User choices, (some) repeated here
ETH_PRIVATE_KEY=0xabcde.....
ETH_ADDRESS=0x123....
# The token to delegate
TOKEN=$EXO_ETH_ADDR
# Quantity of token to deposit (without the 1e18 multiplier)
# Decimal values are supported
QUANTITY=0.5
# Endpoint for Ethereum RPC
ETHEREUM_RPC_URL=https://rpc.ankr.com/eth_sepolia

# Derived value, depending on $ACCOUNT_KEY_NAME and $HOMEDIR in prior steps
EXO_ADDRESS=$(exocored --home $HOMEDIR keys show -a $ACCOUNT_KEY_NAME)

BOOTSTRAPPED=$(cast call --rpc-url $ETHEREUM_RPC_URL $GATEWAY_ADDR "bootstrapped() returns (bool)")
if [ "$BOOTSTRAPPED" = "true" ]; then
    # Generate the LZ messsage to calculate the fees
    DELEGATE_PREFIX="0x03"
    TOKEN_B32=$(cast 2b $TOKEN)
    ETH_ADDRESS_B32=$(cast 2b $ETH_ADDRESS)
    EXO_ADDRESS_BYTES=$(cast fu $EXO_ADDRESS)
    QUANTITY_B32=$(printf "%064s" $(cast 2h $(cast 2w $QUANTITY) | cut -c3-) | tr ' ' '0')
    LZ_MESSAGE=$(cast ch $DELEGATE_PREFIX $TOKEN_B32 $ETH_ADDRESS_B32 $EXO_ADDRESS_BYTES $QUANTITY_B32)
    VALUE=$(cast call --rpc-url $ETHEREUM_RPC_URL $GATEWAY_ADDR "quote(bytes)" $LZ_MESSAGE)
else
    # No LZ fees, since we are in Bootstrap mode
    VALUE=0
fi

# Execute the transaction
cast send --rpc-url $ETHEREUM_RPC_URL \
    $GATEWAY_ADDR \
    "delegateTo(string,address,uint256)" \
    $EXO_ADDRESS \
    $TOKEN \
    $(cast 2w $QUANTITY) \
    --private-key $ETH_PRIVATE_KEY \
    --value $(cast 2d $VALUE)

If the above transaction was executed after the network bootstrap phase, again, LayerZeroScan may be used to query the status of the message. In addition, the withdrawable amount will change on Exocore once the message is delivered.

ETH_LZ_ID=40161 # Sepolia, where our $GATEWAY_ADDR lives
STAKER_ID=$(echo "${ETH_ADDRESS}" | tr '[:upper:]' '[:lower:]')_$(printf '0x%x\n' "${ETH_LZ_ID}")
# our public endpoint is available below, but you can use your own node too
EXOCORE_COS_GRPC_URL=https://api-cosmos-grpc.exocore-restaking.com:443
exocored query assets QueStakerAssetInfos \
    $STAKER_ID --node $EXOCORE_COS_GRPC_URL \
    --output json | jq
# looks like this
{
  "asset_infos": [
    {
      "asset_id": "0x83e6850591425e3c1e263c054f4466838b9bd9e4_0x9ce1",
      "info": {
        "total_deposit_amount": "5000000000000000000",
        "withdrawable_amount": "0", // value of 0
        "pending_undelegation_amount": "0"
      }
    }
  ]
}

Marking a delegation as self-delegation (Post Bootstrap Phase)

This step only applies if the validator registration was done after the Bootstrap phase ended. Otherwise, it is assumed that the delegations made by the Ethereum address, which sent the validator creation transaction, are self-delegations for the validator.

Send the transaction to associate the two addresses: each such association indicates that the delegations made by the Ethereum address are to be considered as self-delegations for the validator.

To prevent slashing for downtime, it is recommended to send the transaction below only after the node is synced.

# User inputs, the first two of which were previously provided
ETH_PRIVATE_KEY=0xabcdefg...
ETH_ADDRESS=0xabcd...

# Constants
EXO_ETH_RPC_URL=https://api-eth.exocore-restaking.com
EXO_COS_GRPC_URL=https://api-cosmos-grpc.exocore-restaking.com:443
ETH_LZ_ID=40161 # Sepolia

# Address of gateway derived from the params of x/assets
EXO_GATEWAY_ADDR=$(exocored query assets Params --output json --node $EXO_COS_GRPC_URL | jq -r .params.exocore_lz_app_address | cast 2a)

# The validator gets only their EXO_ADDRESS funded using the faucet, while the ETH_ADDRESS (if it is different) may not have funds to pay for gas.
ETH_ADDR_BALANCE_IN_HUA=$(cast balance $ETH_ADDRESS --rpc-url $EXO_ETH_RPC_URL)
# The faucet gives out 1exo, of which we will transfer 0.5exo (and retain the rest) to the ETH equivalent address. Note that the ETH equivalent address may be the same as the exo1 address if the same mnemonic is used; hence, the transfer would be pointless. To avoid spending gas on that transfer, we check the balance of the destination account first.
AMOUNT_TO_TRANSFER_IN_HUA=$(cast 2w 0.5 ether)
if [ "$ETH_ADDR_BALANCE_IN_HUA" -lt "$AMOUNT_TO_TRANSFER_IN_HUA" ]; then
    # execute the transfer via cast (using exocored is also possible)
    EXO_PRIVATE_KEY=$(exocored --home $HOMEDIR keys unsafe-export-eth-key $ACCOUNT_KEY_NAME)
    cast send --rpc-url $EXO_ETH_RPC_URL \
        $ETH_ADDRESS \
        --value $AMOUNT_TO_TRANSFER_IN_HUA \
        --private-key $EXO_PRIVATE_KEY
fi

# Send the transaction for the association
cast send --rpc-url $EXO_ETH_RPC_URL \
    $EXO_GATEWAY_ADDR \
    "associateOperatorWithEVMStaker(uint32, string)" \
    $ETH_LZ_ID \
    $(exocored --home $HOMEDIR keys show -a $ACCOUNT_KEY_NAME) \
    --private-key $ETH_PRIVATE_KEY

To check whether the above transaction went through, we can query the association.

ETH_LZ_ID=40161 # Sepolia, where our $GATEWAY_ADDR lives
STAKER_ID=$(echo "${ETH_ADDRESS}" | tr '[:upper:]' '[:lower:]')_$(printf '0x%x\n' "${ETH_LZ_ID}")
# our public endpoint is available below, but you can use your own node too
EXOCORE_COS_GRPC_URL=https://api-cosmos-grpc.exocore-restaking.com:443
exocored query delegation QueryAssociatedOperatorByStaker \
    $STAKER_ID --node $EXOCORE_COS_GRPC_URL \
    --output json | jq
# expected output
{
  "operator": "<your_exo_addr>"
}

The validator should now be considered "eligible" to be part of the active validator set if the self-delegation exceeds 1,000 USD. The inclusion in the set happens in the beginning of the next epoch, which may be at most 1 day from now. Only the top 100 validators (by total stake = self-stake + delegated stake) are included in the validator set.

EPOCH_ID=$(exocored query dogfood params --node $EXOCORE_COS_GRPC_URL --output json | jq -r .params.epoch_identifier)
exocored query epochs epoch-infos --node $EXOCORE_COS_GRPC_URL --output json | jq --arg EPOCH_ID "$EPOCH_ID" '
  . as $root 
  | .epochs[]
  | select(.identifier == $EPOCH_ID)
  | .next_epoch_start_time = (
      (.current_epoch_start_time | fromdateiso8601) + (.duration | sub("s"; "") | tonumber)
    )
  | .time_to_go = (
      (.next_epoch_start_time - ($root.block_time | sub("\\.[0-9]+Z$"; "Z") | fromdateiso8601))
    | if . < 0 then 0 else . end
    )
  | {identifier, next_epoch_start_time: (.next_epoch_start_time | todate), time_to_go: (.time_to_go | tostring + " seconds")}
' # do include this line, which contains only a single apostrophe
# it prints how much time is left for the next epoch, like the below
{
  "identifier": "day",
  "next_epoch_start_time": "2024-09-28T10:45:00Z",
  "time_to_go": "25503 seconds"
}

Last updated