diff --git a/examples/app-ofa-private/README.md b/examples/app-ofa-private/README.md new file mode 100644 index 0000000..a6b2377 --- /dev/null +++ b/examples/app-ofa-private/README.md @@ -0,0 +1,20 @@ + +# Example Suapp for an OFA application with private transactions + +This example features an [Order flow auction](https://collective.flashbots.net/t/order-flow-auctions-and-centralisation-ii-order-flow-auctions/284) Suapp based on the [mev-share](https://github.com/flashbots/mev-share) protocol specification. + +User transactions are stored in the confidential datastore and only a small hint it is leaked outside the Kettle. Then, searchers bid in an order-flow auction for the right to execute against users’ private transactions. + +## How to use + +Run `Suave` in development mode: + +``` +$ suave --suave.dev +``` + +Execute the deployment script: + +``` +$ go run main.go +``` diff --git a/examples/app-ofa-private/main.go b/examples/app-ofa-private/main.go new file mode 100644 index 0000000..0515eb7 --- /dev/null +++ b/examples/app-ofa-private/main.go @@ -0,0 +1,127 @@ +package main + +import ( + "encoding/json" + "fmt" + "io" + "math/big" + "net/http" + "net/http/httptest" + + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/flashbots/suapp-examples/framework" +) + +func main() { + fakeRelayer := httptest.NewServer(&relayHandlerExample{}) + defer fakeRelayer.Close() + + fr := framework.NewFr() + contract := fr.DeployContract("ofa-private.sol/OFAPrivate.json") + + // Step 1. Create and fund the accounts we are going to frontrun/backrun + fmt.Println("1. Create and fund test accounts") + + testAddr1 := framework.GeneratePrivKey() + testAddr2 := framework.GeneratePrivKey() + + fundBalance := big.NewInt(100000000) + fr.FundAccount(testAddr1.Address(), fundBalance) + + targeAddr := testAddr1.Address() + + ethTxn1, _ := fr.SignTx(testAddr1, &types.LegacyTx{ + To: &targeAddr, + Value: big.NewInt(1000), + Gas: 21000, + GasPrice: big.NewInt(13), + }) + + ethTxnBackrun, _ := fr.SignTx(testAddr2, &types.LegacyTx{ + To: &targeAddr, + Value: big.NewInt(1000), + Gas: 21420, + GasPrice: big.NewInt(13), + }) + + // Step 2. Send the initial transaction + fmt.Println("2. Send bid") + + refundPercent := 10 + bundle := &types.SBundle{ + Txs: types.Transactions{ethTxn1}, + RevertingHashes: []common.Hash{}, + RefundPercent: &refundPercent, + } + bundleBytes, _ := json.Marshal(bundle) + + // new bid inputs + receipt := contract.SendTransaction("newOrder", []interface{}{}, bundleBytes) + + hintEvent := &HintEvent{} + if err := hintEvent.Unpack(receipt.Logs[0]); err != nil { + panic(err) + } + + fmt.Println("Hint event id", hintEvent.BidId) + + // Step 3. Send the backrun transaction + fmt.Println("3. Send backrun") + + backRunBundle := &types.SBundle{ + Txs: types.Transactions{ethTxnBackrun}, + RevertingHashes: []common.Hash{}, + } + backRunBundleBytes, _ := json.Marshal(backRunBundle) + + // backrun inputs + receipt = contract.SendTransaction("newMatch", []interface{}{hintEvent.BidId}, backRunBundleBytes) + + matchEvent := &HintEvent{} + if err := matchEvent.Unpack(receipt.Logs[0]); err != nil { + panic(err) + } + + fmt.Println("Match event id", matchEvent.BidId) + + // Step 4. Emit the batch to the relayer + fmt.Println("Step 4. Emit batch") + + contract.SendTransaction("emitMatchBidAndHint", []interface{}{fakeRelayer.URL, matchEvent.BidId}, backRunBundleBytes) +} + +var hintEventABI abi.Event + +func init() { + artifact, _ := framework.ReadArtifact("ofa-private.sol/OFAPrivate.json") + hintEventABI = artifact.Abi.Events["HintEvent"] +} + +type HintEvent struct { + BidId [16]byte + Hint []byte +} + +func (h *HintEvent) Unpack(log *types.Log) error { + unpacked, err := hintEventABI.Inputs.Unpack(log.Data) + if err != nil { + return err + } + h.BidId = unpacked[0].([16]byte) + h.Hint = unpacked[1].([]byte) + return nil +} + +type relayHandlerExample struct { +} + +func (rl *relayHandlerExample) ServeHTTP(w http.ResponseWriter, r *http.Request) { + bodyBytes, err := io.ReadAll(r.Body) + if err != nil { + panic(err) + } + + fmt.Println(string(bodyBytes)) +} diff --git a/examples/app-ofa-private/ofa-private.sol b/examples/app-ofa-private/ofa-private.sol new file mode 100644 index 0000000..99d90c2 --- /dev/null +++ b/examples/app-ofa-private/ofa-private.sol @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.8; + +import "../../suave-geth/suave/sol/libraries/Suave.sol"; + +contract OFAPrivate { + address[] public addressList = [0xC8df3686b4Afb2BB53e60EAe97EF043FE03Fb829]; + + // Struct to hold hint-related information for an order. + struct HintOrder { + Suave.BidId id; + bytes hint; + } + + event HintEvent ( + Suave.BidId id, + bytes hint + ); + + // Internal function to save order details and generate a hint. + function saveOrder() internal view returns (HintOrder memory) { + // Retrieve the bundle data from the confidential inputs + bytes memory bundleData = Suave.confidentialInputs(); + + // Simulate the bundle and extract its score. + uint64 egp = Suave.simulateBundle(bundleData); + + // Extract a hint about this bundle that is going to be leaked + // to external applications. + bytes memory hint = Suave.extractHint(bundleData); + + // Store the bundle and the simulation results in the confidential datastore. + Suave.Bid memory bid = Suave.newBid(10, addressList, addressList, ""); + Suave.confidentialStore(bid.id, "mevshare:v0:ethBundles", bundleData); + Suave.confidentialStore(bid.id, "mevshare:v0:ethBundleSimResults", abi.encode(egp)); + + HintOrder memory hintOrder; + hintOrder.id = bid.id; + hintOrder.hint = hint; + + return hintOrder; + } + + function emitHint(HintOrder memory order) public payable { + emit HintEvent(order.id, order.hint); + } + + // Function to create a new user order + function newOrder() external payable returns (bytes memory) { + HintOrder memory hintOrder = saveOrder(); + return abi.encodeWithSelector(this.emitHint.selector, hintOrder); + } + + // Function to match and backrun another bid. + function newMatch(Suave.BidId shareBidId) external payable returns (bytes memory) { + HintOrder memory hintOrder = saveOrder(); + + // Merge the bids and store them in the confidential datastore. + // The 'fillMevShareBundle' precompile will use this information to send the bundles. + Suave.BidId[] memory bids = new Suave.BidId[](2); + bids[0] = shareBidId; + bids[1] = hintOrder.id; + Suave.confidentialStore(hintOrder.id, "mevshare:v0:mergedBids", abi.encode(bids)); + + return abi.encodeWithSelector(this.emitHint.selector, hintOrder); + } + + function emitMatchBidAndHintCallback() external payable { + } + + function emitMatchBidAndHint(string memory builderUrl, Suave.BidId bidId) external payable returns (bytes memory) { + bytes memory bundleData = Suave.fillMevShareBundle(bidId); + Suave.submitBundleJsonRPC(builderUrl, "mev_sendBundle", bundleData); + + return abi.encodeWithSelector(this.emitMatchBidAndHintCallback.selector); + } +} diff --git a/examples/mevm-confidential-store/main.go b/examples/mevm-confidential-store/main.go index 4db9fb6..a0a6ed9 100644 --- a/examples/mevm-confidential-store/main.go +++ b/examples/mevm-confidential-store/main.go @@ -5,5 +5,7 @@ import ( ) func main() { - framework.DeployAndTransact("confidential-store.sol/ConfidentialStore.json", "example") + fr := framework.New() + fr.DeployContract("confidential-store.sol/ConfidentialStore.json"). + SendTransaction("example", nil, nil) } diff --git a/examples/mevm-external-uniswap-v3-quote/external-uniswap-v3-quote.sol b/examples/mevm-external-uniswap-v3-quote/external-uniswap-v3-quote.sol index dbfdf01..52e661f 100644 --- a/examples/mevm-external-uniswap-v3-quote/external-uniswap-v3-quote.sol +++ b/examples/mevm-external-uniswap-v3-quote/external-uniswap-v3-quote.sol @@ -30,7 +30,12 @@ library UniswapV3 { } contract ExternalUniswapV3Quote { - function example(UniswapV3.ExactOutputSingleParams memory params) external payable { + function callback() external payable { + } + + function example(UniswapV3.ExactOutputSingleParams memory params) external payable returns (bytes memory) { UniswapV3.exactOutputSingle(params); + + return abi.encodeWithSelector(this.callback.selector); } } diff --git a/examples/mevm-is-confidential/main.go b/examples/mevm-is-confidential/main.go index c042132..68bb834 100644 --- a/examples/mevm-is-confidential/main.go +++ b/examples/mevm-is-confidential/main.go @@ -5,5 +5,7 @@ import ( ) func main() { - framework.DeployAndTransact("is-confidential.sol/IsConfidential.json", "example") + fr := framework.New() + fr.DeployContract("is-confidential.sol/IsConfidential.json"). + SendTransaction("example", nil, nil) } diff --git a/examples/onchain-callback/main.go b/examples/onchain-callback/main.go index cca9a23..b670fe1 100644 --- a/examples/onchain-callback/main.go +++ b/examples/onchain-callback/main.go @@ -5,5 +5,7 @@ import ( ) func main() { - framework.DeployAndTransact("onchain-callback.sol/OnChainCallback.json", "example") + fr := framework.New() + fr.DeployContract("onchain-callback.sol/OnChainCallback.json"). + SendTransaction("example", nil, nil) } diff --git a/framework/framework.go b/framework/framework.go index 1a273b0..1567689 100644 --- a/framework/framework.go +++ b/framework/framework.go @@ -1,10 +1,12 @@ package framework import ( + "context" "crypto/ecdsa" "encoding/hex" "encoding/json" "fmt" + "math/big" "os" "path/filepath" "runtime" @@ -58,87 +60,146 @@ func ReadArtifact(path string) (*Artifact, error) { return art, nil } -var ( - ExNodeEthAddr = common.HexToAddress("b5feafbdd752ad52afb7e1bd2e40432a485bbb7f") - ExNodeNetAddr = "http://localhost:8545" - - // This account is funded in both devnev networks - // address: 0xBE69d72ca5f88aCba033a063dF5DBe43a4148De0 - FundedAccount = newPrivKeyFromHex("91ab9a7e53c220e6210460b65a7a3bb2ca181412a8a7b43ff336b3df1737ce12") -) - -type privKey struct { +type PrivKey struct { Priv *ecdsa.PrivateKey } -func (p *privKey) Address() common.Address { +func (p *PrivKey) Address() common.Address { return crypto.PubkeyToAddress(p.Priv.PublicKey) } -func (p *privKey) MarshalPrivKey() []byte { +func (p *PrivKey) MarshalPrivKey() []byte { return crypto.FromECDSA(p.Priv) } -func newPrivKeyFromHex(hex string) *privKey { +func NewPrivKeyFromHex(hex string) *PrivKey { key, err := crypto.HexToECDSA(hex) if err != nil { panic(fmt.Sprintf("failed to parse private key: %v", err)) } - return &privKey{Priv: key} + return &PrivKey{Priv: key} +} + +func GeneratePrivKey() *PrivKey { + key, err := crypto.GenerateKey() + if err != nil { + panic(fmt.Sprintf("failed to generate private key: %v", err)) + } + return &PrivKey{Priv: key} +} + +type Contract struct { + *sdk.Contract +} + +func (c *Contract) SendTransaction(method string, args []interface{}, confidentialBytes []byte) *types.Receipt { + txnResult, err := c.Contract.SendTransaction(method, args, confidentialBytes) + if err != nil { + panic(err) + } + receipt, err := txnResult.Wait() + if err != nil { + panic(err) + } + if receipt.Status == 0 { + panic("bad") + } + return receipt +} + +type Framework struct { + config *Config + rpc *rpc.Client + clt *sdk.Client +} + +type Config struct { + KettleRPC string + KettleAddr common.Address + FundedAccount *PrivKey +} + +func DefaultConfig() *Config { + return &Config{ + KettleRPC: "http://localhost:8545", + KettleAddr: common.HexToAddress("b5feafbdd752ad52afb7e1bd2e40432a485bbb7f"), + + // This account is funded in both devnev networks + // address: 0xBE69d72ca5f88aCba033a063dF5DBe43a4148De0 + FundedAccount: NewPrivKeyFromHex("91ab9a7e53c220e6210460b65a7a3bb2ca181412a8a7b43ff336b3df1737ce12"), + } } -func DeployContract(path string) (*sdk.Contract, error) { - rpc, _ := rpc.Dial(ExNodeNetAddr) - mevmClt := sdk.NewClient(rpc, FundedAccount.Priv, ExNodeEthAddr) +func New() *Framework { + config := DefaultConfig() + rpc, _ := rpc.Dial(config.KettleRPC) + clt := sdk.NewClient(rpc, config.FundedAccount.Priv, config.KettleAddr) + + return &Framework{ + config: DefaultConfig(), + rpc: rpc, + clt: clt, + } +} + +func (f *Framework) DeployContract(path string) *Contract { artifact, err := ReadArtifact(path) if err != nil { - return nil, err + panic(err) } // deploy contract - txnResult, err := sdk.DeployContract(artifact.Code, mevmClt) + txnResult, err := sdk.DeployContract(artifact.Code, f.clt) if err != nil { - return nil, err + panic(err) } - receipt, err := ensureTransactionSuccess(txnResult) + receipt, err := txnResult.Wait() if err != nil { - return nil, err + panic(err) + } + if receipt.Status == 0 { + panic(fmt.Errorf("transaction failed")) } - contract := sdk.GetContract(receipt.ContractAddress, artifact.Abi, mevmClt) - return contract, nil + contract := sdk.GetContract(receipt.ContractAddress, artifact.Abi, f.clt) + return &Contract{Contract: contract} } -func ensureTransactionSuccess(txn *sdk.TransactionResult) (*types.Receipt, error) { - receipt, err := txn.Wait() +func (f *Framework) SignTx(priv *PrivKey, tx *types.LegacyTx) (*types.Transaction, error) { + rpc, _ := rpc.Dial("http://localhost:8545") + + cltAcct1 := sdk.NewClient(rpc, priv.Priv, common.Address{}) + signedTxn, err := cltAcct1.SignTxn(tx) if err != nil { return nil, err } - if receipt.Status == 0 { - return nil, err - } - return receipt, nil + return signedTxn, nil } -// DeployAndTransact is a helper function that deploys a suapp -// and inmediately executes a function on it with a confidential request. -func DeployAndTransact(path, funcName string) { - contract, err := DeployContract(path) +var errFundAccount = fmt.Errorf("failed to fund account") + +func (f *Framework) FundAccount(to common.Address, value *big.Int) error { + txn := &types.LegacyTx{ + Value: value, + To: &to, + } + result, err := f.clt.SendTransaction(txn) if err != nil { - fmt.Printf("failed to deploy contract: %v", err) - os.Exit(1) + return err } - - txnResult, err := contract.SendTransaction(funcName, []interface{}{}, []byte{}) + _, err = result.Wait() if err != nil { - fmt.Printf("failed to send transaction: %v", err) - os.Exit(1) + return err } - - if _, err = ensureTransactionSuccess(txnResult); err != nil { - fmt.Printf("failed to ensure transaction success: %v", err) - os.Exit(1) + // check balance + balance, err := f.clt.RPC().BalanceAt(context.Background(), to, nil) + if err != nil { + return err + } + if balance.Cmp(value) != 0 { + return errFundAccount } + return nil }