1. What
EIP712 signing is the system's primary method for authentication, it allows a user to sign a message with their private key, providing a signature, this signature is then validated by the off-chain systems AND smart contracts against the message to make sure the signer is either the account being interacted with or authorised to interact with the specified account. These signatures in combination with the on-chain smart contracts allow for a self-custodial experience for users and make it impossible for other users and the off-chain system to touch a user's funds without authorisation (so long as a user's private key is not compromised).
For more details on EIP712 signing and its safety read the following: https://eips.ethereum.org/EIPS/eip-712
Domain Seperator
The domain separator must be attached to every EIP712 message, the information in this struct is static for each chain. For the rysk EIP712Domain:
- The
name
parameter is "rysk" - The
version
is "0.0.0", - The
chainId
is 421614 for ARBITRUM SEPOLIA TESTNET and 42161 for ARBITRUM Mainnet - The
verifyingContract
This is the OrderDispatch contract. (0x6644D5B09EBae015fE4e3a87Eff1A07d33558E59)
{
EIP712Domain: [
{
name: 'name', // rysk
type: 'string',
},
{
name: 'version', // 0.0.0
type: 'string',
},
{
name: 'chainId', // 421614 - ARBITRUM testnet || 42161 - ARBITRUM mainnet
type: 'uint256',
},
{
name: 'verifyingContract', // OrderDispatch 0x6644D5B09EBae015fE4e3a87Eff1A07d33558E59
type: 'address',
},
],
}
EIP712 Types (Messages)
The messages that can be signed are provided below, descriptions of each element are provided.
{
LoginMessage: [ // used to authenticate a login by the user
{
name: 'account', // address of the account logging in
type: 'address',
},
{
name: 'message', // e.g. "I want to log into rysk.finance"
type: 'string',
},
{
name: 'timestamp', // current timestamp in ms (will be rejected if older than 10s, easiest to send in a time in the future)
type: 'uint64',
},
],
Withdraw: [ // used as authentication for a withdrawal request by the account
{
name: "account", // address of the account withdrawing
type: "address",
},
{
name: "subAccountId", // subAccountId of the subaccount to withdraw from
type: "uint8",
},
{
name: "asset", // address of the asset in the margin account to withdraw
type: "address",
},
{
name: "quantity", // quantity to withdraw in e18 format
type: "uint128",
},
{
name: "nonce", // unique nonce for the transaction, nonces are tracked globally across all actions and cannot be repeated
type: "uint64",
},
],
Order: [ // used as authentication for order creation by the subaccount
{
name: "account", // address of the account making the order
type: "address",
},
{
name: "subAccountId", // subAccountId of the subaccount to execute the order on
type: "uint8",
},
{
name: "productId", // the productId of the product to trade
type: "uint32",
},
{
name: "isBuy", // whether the account is buying or selling
type: "bool",
},
{
name: "orderType", // e.g. limit, market, etc.
type: "uint8",
},
{
name: "timeInForce", // e.g. GTC, IOC, FOK
type: "uint8",
},
{
name: "expiration", // the expiration of the order, after this time the order is dropped from the orderbook
type: "uint64",
},
{
name: "price", // the price to execute the trade at represented in the quote asset in e18 format (the trade will be executed at this price or better)
type: "uint128",
},
{
name: "quantity", // the number of contracts to purchase at the given price
type: "uint128",
},
{
name: "nonce", // unique nonce for the transaction, nonces are tracked globally across all actions and cannot be repeated
type: "uint64",
},
],
ApproveSigner: [ // used as authentication for approving a signer to sign transactions on behalf of your subaccount
{
name: "account", // address of the account to approve the approvedSigner on
type: "address",
},
{
name: "subAccountId", // subAccountId of the subaccount to approve the signer on
Type: "uint8",
},
{
name: "approvedSigner", // the address of the account that will be an approvedSigner on the given subaccount
type: "address",
},
{
name: "isApproved", // boolean for whether to approve the approvedSigner on the account or not
type: "bool",
},
{
name: "nonce", // unique nonce for the transaction, nonces are tracked globally across all actions and cannot be repeated
type: "uint64",
},
],
CancelOrder: [ // used as authentication for cancelling an order
{
name: "account", // address of the account to cancel the order on
type: "address",
},
{
name: "subAccountId", // subAccountId of the subaccount to cancel the order on
type: "uint8",
},
{
name: "productId", // the productId of the product to cancel the order on (retrievable from list products)
type: "uint32",
},
{
name: "orderId", // id of the order to cancel
type: "string",
},
],
CancelOrders: [ // used as authentication for cancelling all orders
{
name: "account", // address of the account to cancel all orders on
type: "address",
},
{
name: "subAccountId", // subAccountId of the subaccount to cancel all orders on
type: "uint8",
},
{
name: "productId", // the productId of the product to cancel all orders on (retrievable from list products)
type: "uint32",
},
]
SignedAuthentication: [
{
name: "account", // address to view
type: "address",
},
{
name: "subAccountId", // subAccountId of the subaccount to view
type: "uint8",
],
},
}
var EIP712_TYPES = &apitypes.Types{
"EIP712Domain": {
{
Name: "name", // Ciao
Type: "string",
},
{
Name: "version", // "0.0.0"
Type: "string",
},
{
Name: "chainId", // 168587773 - Blast testnet |
Type: "uint256",
},
{
Name: "verifyingContract", // ORDER_DISPATCH_ADDRESS
Type: "address",
},
},
"LoginMessage": {
{
Name: "account",
Type: "address",
},
{
Name: "message",
Type: "string",
},
{
Name: "timestamp",
Type: "uint64",
},
},
"Order": {
{
Name: "account",
Type: "address",
},
{
Name: "subAccountId",
Type: "uint8",
},
{
Name: "productId",
Type: "uint32",
},
{
Name: "isBuy",
Type: "bool",
},
{
Name: "orderType",
Type: "uint8",
},
{
Name: "timeInForce",
Type: "uint8",
},
{
Name: "expiration",
Type: "uint64",
},
{
Name: "price",
Type: "uint128",
},
{
Name: "quantity",
Type: "uint128",
},
{
Name: "nonce",
Type: "uint64",
},
},
"CancelOrders": {
{
Name: "account",
Type: "address",
},
{
Name: "subAccountId",
Type: "uint8",
},
{
Name: "productId",
Type: "uint32",
},
},
"CancelOrder": {
{
Name: "account",
Type: "address",
},
{
Name: "subAccountId",
Type: "uint8",
},
{
Name: "productId",
Type: "uint32",
},
{
Name: "orderId",
Type: "string",
},
},
"ApproveSigner": {
{
Name: "account",
Type: "address",
},
{
Name: "subAccountId",
Type: "uint8",
},
{
Name: "approvedSigner",
Type: "address",
},
{
Name: "isApproved",
Type: "bool",
},
{
Name: "nonce",
Type: "uint64",
},
},
"Deposit": {
{
Name: "account",
Type: "address",
},
{
Name: "subAccountId",
Type: "uint8",
},
{
Name: "asset",
Type: "address",
},
{
Name: "quantity",
Type: "uint256",
},
{
Name: "nonce",
Type: "uint64",
},
},
"Withdraw": {
{
Name: "account",
Type: "address",
},
{
Name: "subAccountId",
Type: "uint8",
},
{
Name: "asset",
Type: "address",
},
{
Name: "quantity",
Type: "uint128",
},
{
Name: "nonce",
Type: "uint64",
},
},
"SignedAuthentication": {
{
Name: "account",
Type: "address",
},
{
Name: "subAccountId",
Type: "uint8",
},
},
Signing for Authenticating HTTP requests - Tutorial
This tutorial will use Python as an example for constructing signatures for action authentication but the process is mostly the same for all languages, so just follow the comments, examples for other languages will be added upon request. The example goes through creating an order.
Required Python packages:
import eth_account # used for creating an account and signing the message using a stored private key
from eip712_structs import Address, EIP712Struct, String, Uint, make_domain # used for message construction https://pypi.org/project/eip712-structs/
from eth_account.messages import encode_structured_data # used for message construction
import time
-
Defining the EIP712 type, EIP712 domain and private key
# these data types can be seen in the type descriptions at the bottom of this page, # make sure the datatypes are identical class Order(EIP712Struct): account = Address() subAccountId = Uint(8) productId = Uint(32) isBuy = Boolean() orderType = Uint(8) timeInForce = Uint(8) expiration = Uint(64) price = Uint(128) quantity = Uint(128) nonce = Uint(64) def create_testnet_domain(): # this is static for each chain so can be defined once for all time, always # the make_domain function from the eip712_structs constructs and returns the domain seperator return make_domain(name="rysk", version="0.0.0", chainId=421614, verifyingContract="0xInsertAddressHere") def create_wallet(private_key: str): # this is the wallet used for signing, and must be derived from a private key, # typically presented in a .env return eth_account.Account.from_key(private_key)
-
Generating the signature used for authentication takes two steps, building the hashed message and signing that message
def generate_signature(public_key: str, private_key: str):
wallet = create_wallet(private_key)
# we first want to build the message hash, we start by constructing the Order EIP712 struct we
# made earlier passing in our desired data for the order in the struct, this is hardcoded here
# for ease of reading
order = Order(account = public_key, # make sure this address is checksumed
subAccountId= 217, # the subaccount you want to work with
productId = 1002, # the numeric product id to trade, can be retrieved from list products
isBuy = true, # buying
orderType = 0, # Limit order
timeInForce = 0, # GTC
expiration = int(time.time() * 1000) + 86400000, # order expires tomorrow
price = 3000000000000000000000, # $3000 per contract or better
quantity = 5000000000000000000, # 5 contracts
nonce = 761398176 # we recommend using the current time in microseconds for this, a number less than uint64 that has never been used for any other action
)
# we construct the order message using the to_message function which the Order class we made earlier
# inherited from the EIP712Struct class, using the domain seperator as a parameter
order_message = order.to_message(create_testnet_domain())
# we hash/encode the data using eth_account.messages encode_structured_data, ready for signing
signable_order_message = encode_structured_data(order_message)
# we sign the message
signed_order = wallet.sign_message(signable_order_message)
# we extract the signature and return it (this signature is added to the body of the request,
# the order details passed in the body of the request must be the same values you passed in to
# generate the signature, so the hardcoded values we provided in this example
# for the datatype, just pass in whatever the rest http request doc expects and the datatypes
# will get properly converted for signature validation on the other side
signature = signed_order.signature.hex()
# this is the signature in hex format that should be added to your request and acts as order authentication
return signature