Skip to main content

How to Participate in Auctions

After bonding atlETH and deploying their smart contract, solvers are finally able to participate in auctions, by communicating with the operations-relay.

This guide is using Go and the Atlas Go SDK.

1. Communicate with the Operations-Relay

The operations-relay exposes a compliant JSON RPC API, so we can use the geth RPC client. The following code connects to the operations-relay and defines an event loop where user operation notifications will be received.

It validates the notification and builds a solver operation by calling the isOfInterest and buildSolution functions, which are defined in the next sections. It then sends the solution back to the operations-relay.

connect.go
package operations_relay

import (
"context"
"time"

"github.com/FastLane-Labs/atlas-sdk-go/types"
"github.com/ethereum/go-ethereum/rpc"
"github.com/gorilla/websocket"
)

const (
solverNamespace = "solver"
userOperationsSubscriptionTopic = "userOperations"
submitSolverOperationMethod = "solver_submitSolverOperation"
operationsRelayUrl = "wss://relay-fra.fastlane-labs.xyz/ws/solver"
)

// Notifications are received in this format
type UserOperationNotification struct {
AuctionId string `json:"auction_id"`
PartialUserOperation *types.UserOperationPartialRaw `json:"partial_user_operation"`
}

func runOperationsRelay() {
// Set bigger buffer sizes for the websocket connection
dialerOption := rpc.WithWebsocketDialer(websocket.Dialer{
ReadBufferSize: 1024 * 1024,
WriteBufferSize: 1024 * 1024,
})

// Dial the operations relay
operationsRelayClient, err := rpc.DialOptions(context.TODO(), operationsRelayUrl, dialerOption)
if err != nil {
panic(err)
}

// Create a channel to receive notifications
var (
uoChan chan *UserOperationNotification
sub *rpc.ClientSubscription
)

// Subscribe/Resubscribe to user operations notifications
subscribe := func() {
for {
uoChan = make(chan *UserOperationNotification, 32)

sub, err = operationsRelayClient.Subscribe(context.TODO(), solverNamespace, uoChan, userOperationsSubscriptionTopic)
if err != nil {
// Failed to subscribe, wait a bit and retry
time.Sleep(1 * time.Second)
continue
}

break
}
}

// Main loop
for {
select {
case <-sub.Err():
// If the subscription errors, resubscribe
subscribe()

case n := <-uoChan:
// Received a notification, check if it's of interest
if !isOfInterest(n) {
continue
}

// Build a solution and submit it
solution, err := buildSolution(n)
if err != nil {
panic(err)
}

// No error means the submission was successful
err = operationsRelayClient.CallContext(context.TODO(), nil, submitSolverOperationMethod, solution)
if err != nil {
panic(err)
}
}
}
}

2. Build solver operations

Here is an example on how to construct a solver operation.

solution.go
package operations_relay

import (
"math/big"

"github.com/FastLane-Labs/atlas-sdk-go/config"
"github.com/FastLane-Labs/atlas-sdk-go/types"
"github.com/FastLane-Labs/atlas-sdk-go/utils"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
)

// Solutions are sent in this format
type Solution struct {
AuctionId string `json:"auction_id"`
AuctionSolution *types.SolverOperationRaw `json:"auction_solution"`
}

func buildSolution(n *UserOperationNotification) (*Solution, error) {
// Get the solver private key
solverPk, err := crypto.HexToECDSA("0xmySolverPrivateKey")
if err != nil {
return nil, err
}

var (
// This is the address of the solver account
// This account must have bonded atlETH on the Atlas contract
solverAccount = crypto.PubkeyToAddress(solverPk.PublicKey)

// This is the address of the solver contract
// This contract must have the `atlasSolverCall` defined
solverContractAddress = common.HexToAddress("0xmySolverContractAddress")

// This contains the data to be passed to the `atlasSolverCall`
// This should contain the solver execution logic
solverData = common.FromHex("0xmySolverData")
)

// Build the solver operation
solverOperation := &types.SolverOperation{
// The solver account (and signer of this operation)
From: solverAccount,

// The Atlas address, here we're retrieving it from the user operation
To: n.PartialUserOperation.To,

// The gas limit for the solver operation
Gas: big.NewInt(500_000),

// The max fee per gas for the solver operation
// It must be equal to or higher than the user operation's max fee per gas
// Here we match the user operation's value
MaxFeePerGas: new(big.Int).Set(n.PartialUserOperation.MaxFeePerGas.ToInt()),

// The deadline for the solver operation (block number)
// Here we match the user operation's value
Deadline: new(big.Int).Set(n.PartialUserOperation.Deadline.ToInt()),

// The solver contract address
Solver: solverContractAddress,

// The `dAppControl` (module) address
// Here we're retrieving it from the user operation
Control: n.PartialUserOperation.Control,

// The user operation hash
UserOpHash: n.PartialUserOperation.UserOpHash,

// The bid token, address(0) usually stands for ETH
BidToken: common.Address{},

// The bid amount
BidAmount: big.NewInt(200_000),

// The data to be passed to the solver contract
Data: solverData,
}

// To sign an operation with the SDK, we need to specify the chain ID and the Atlas version

// Getting the chain ID from the notification
chainId := n.PartialUserOperation.ChainId.ToInt().Uint64()

// Getting the Atlas version from the Atlas address specified in the user operation
atlasVersion, err := config.GetVersionFromAtlasAddress(chainId, n.PartialUserOperation.To)
if err != nil {
return nil, err
}

// Getting the hash of the solver operation (this is the payload that will be signed)
hash, err := solverOperation.Hash(chainId, &atlasVersion)
if err != nil {
return nil, err
}

// Signing the hash with the solver private key
signature, err := utils.SignMessage(hash.Bytes(), solverPk)
if err != nil {
return nil, err
}

// Setting the signature to the solver operation
solverOperation.Signature = signature

// Returning the solution
return &Solution{
// The auction ID, as communicated in the notification
AuctionId: n.AuctionId,

// The solver operation, serialized
AuctionSolution: solverOperation.EncodeToRaw(),
}, nil
}

3. Filter user operations

In the above example, we do not filter user operations. The operations-relay will broadcast every user operation it gets. It's necessary to discard operations that the solver can't bid on.

filter.go
package operations_relay

import (
"math/big"

"github.com/ethereum/go-ethereum/common"
)

var (
// Filtering notifications for this chain ID only
chainId = big.NewInt(1)

// Filtering notifications for this dApp control (module) address only
dAppControlAddress = common.HexToAddress("0xmyFavoriteDAppControlAddress")
)

// This function returns true if the notification is of interest and false
// if it should be discarded
func isOfInterest(n *UserOperationNotification) bool {
if n.PartialUserOperation.ChainId.ToInt().Cmp(chainId) != 0 {
return false
}

if n.PartialUserOperation.Control != dAppControlAddress {
return false
}

return true
}

4. Decode User Operations

When a notification is of interest, and before we start building our solver operation, we must decode the received user operation and ensure we're able to bid on it.

To find out what a user operation is intending to do, we should look into its dapp and data fields.

The userOperation.dapp is the contract address that will be called by the user. The userOperation.data is the calldata that will be passed to that call (including function selector).

From this data, the solver must be able to make the appropriate calculations/simulations, and decide whether the user operation is worth bidding on.

warning

Note that the operations-relay broadcasts partial user operations. In some cases, as per dAppControl (module) rules, the data field of a user operation can be concealed. In that case, the hints field will be present (it can be one or the other, but never both fields can be set at the same time).

In such cases, the solver will need to know how to properly interpret the hints provided by the dApp, and submit an ex-post bid solution, which is covered in another guide.