From 017221d8f5e684b9e3cfac13db6d930f3c14bdc3 Mon Sep 17 00:00:00 2001 From: corver <29249923+corverroos@users.noreply.github.com> Date: Mon, 2 Dec 2024 23:30:12 +0200 Subject: [PATCH] chore(solver/app): approve outbox spend in fulfil (#2572) Before submitting fulfil to outbox, approve it to spend token prereqs. issue: #2571 --------- Co-authored-by: Kevin Halliday --- contracts/Makefile | 1 + e2e/app/run.go | 8 ++ e2e/solve/deploy.go | 6 +- e2e/solve/devapp/deploy.go | 112 +++++++++++++++--- e2e/solve/devapp/deposit.go | 120 ++++++++++++++++++- e2e/solve/devapp/deposit_internal_test.go | 27 +++++ e2e/solve/devapp/devapp.go | 8 +- e2e/solve/devapp/target.go | 25 +++- e2e/test/solve_test.go | 44 +------ lib/cast/cast.go | 11 +- lib/umath/umath.go | 12 ++ lib/umath/umath_test.go | 12 ++ solver/app/app.go | 4 +- solver/app/helpers.go | 2 +- solver/app/{deps.go => procdeps.go} | 137 ++++++++++++++++++++-- solver/types/target.go | 5 + 16 files changed, 454 insertions(+), 80 deletions(-) create mode 100644 e2e/solve/devapp/deposit_internal_test.go rename solver/app/{deps.go => procdeps.go} (60%) diff --git a/contracts/Makefile b/contracts/Makefile index c515537be..0e970bb05 100644 --- a/contracts/Makefile +++ b/contracts/Makefile @@ -26,6 +26,7 @@ install-deps: check-pnpm-version ## Install dependencies. build: version ## Build contracts. forge build --force --root core forge build --force --root avs + forge build --force --root solve .PHONY: all diff --git a/e2e/app/run.go b/e2e/app/run.go index 681b47f5e..8a4ae38f5 100644 --- a/e2e/app/run.go +++ b/e2e/app/run.go @@ -9,6 +9,7 @@ import ( "github.com/omni-network/omni/e2e/netman" "github.com/omni-network/omni/e2e/netman/pingpong" "github.com/omni-network/omni/e2e/solve" + "github.com/omni-network/omni/e2e/solve/devapp" "github.com/omni-network/omni/e2e/types" "github.com/omni-network/omni/halo/genutil/evm/predeploys" "github.com/omni-network/omni/lib/contracts" @@ -171,6 +172,13 @@ func E2ETest(ctx context.Context, def Definition, cfg E2ETestConfig) error { return err } + if def.Manifest.DeploySolve { + // TODO(corver): Remove this + if err := devapp.TestFlow(ctx, NetworkFromDef(def), ExternalEndpoints(def)); err != nil { + return err + } + } + var eg errgroup.Group eg.Go(func() error { return testGasPumps(ctx, def) }) eg.Go(func() error { return testBridge(ctx, def) }) diff --git a/e2e/solve/deploy.go b/e2e/solve/deploy.go index 12944011c..88b97215f 100644 --- a/e2e/solve/deploy.go +++ b/e2e/solve/deploy.go @@ -26,7 +26,11 @@ func DeployContracts(ctx context.Context, network netconf.Network, backends ethb var eg errgroup.Group eg.Go(func() error { - return deployBoxes(ctx, network, backends) + if err := deployBoxes(ctx, network, backends); err != nil { + return errors.Wrap(err, "deploy boxes") + } + + return devapp.AllowOutboxCalls(ctx, network, backends) }) eg.Go(func() error { return devapp.Deploy(ctx, network, backends) diff --git a/e2e/solve/devapp/deploy.go b/e2e/solve/devapp/deploy.go index 936c177dd..d6dde4bef 100644 --- a/e2e/solve/devapp/deploy.go +++ b/e2e/solve/devapp/deploy.go @@ -5,6 +5,7 @@ import ( "github.com/omni-network/omni/contracts/bindings" "github.com/omni-network/omni/e2e/app/eoa" + "github.com/omni-network/omni/lib/cast" "github.com/omni-network/omni/lib/contracts" "github.com/omni-network/omni/lib/create3" "github.com/omni-network/omni/lib/errors" @@ -13,6 +14,9 @@ import ( "github.com/omni-network/omni/lib/netconf" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/params" + + "cosmossdk.io/math" ) const ( @@ -21,25 +25,83 @@ const ( l2TokenSalt = "l2-token" ) -var ( - create3Factory = contracts.Create3Factory(netconf.Devnet) - deployer = eoa.MustAddress(netconf.Devnet, eoa.RoleDeployer) -) - // Deploy deploys the mock tokens and vaults to devnet. func Deploy(ctx context.Context, network netconf.Network, backends ethbackend.Backends) error { if network.ID != netconf.Devnet { return errors.New("onl devnet") } - mockl1, ok := network.Chain(evmchain.IDMockL1) - if !ok { - return errors.New("no mock l1") + l1Backend, err := backends.Backend(static.L1.ChainID) + if err != nil { + return errors.Wrap(err, "backend mock l1") + } + + l2Backend, err := backends.Backend(static.L2.ChainID) + if err != nil { + return errors.Wrap(err, "backend mock l2") + } + + if err := deployToken(ctx, l1Backend, l1TokenSalt); err != nil { + return errors.Wrap(err, "deploy l1 token") + } + + if err := deployVault(ctx, l1Backend, l1VaultSalt, static.L1Token); err != nil { + return errors.Wrap(err, "deploy vault") + } + + if err := deployToken(ctx, l2Backend, l2TokenSalt); err != nil { + return errors.Wrap(err, "deploy l2 token") + } + + if err := fundSolver(ctx, l1Backend, static.L1Token); err != nil { + return errors.Wrap(err, "fund solver") + } + + return nil +} + +func fundSolver(ctx context.Context, backend *ethbackend.Backend, tokenAddr common.Address) error { + mngr := eoa.MustAddress(netconf.Devnet, eoa.RoleManager) // we use mngr to mint, but this doesn't matter. could be any dev addr + slvr := eoa.MustAddress(netconf.Devnet, eoa.RoleSolver) + + token, err := bindings.NewMockToken(tokenAddr, backend) + if err != nil { + return errors.Wrap(err, "new mock token") + } + + txOpts, err := backend.BindOpts(ctx, mngr) + if err != nil { + return errors.Wrap(err, "bind opts") } - mockl2, ok := network.Chain(evmchain.IDMockL2) + eth1m := math.NewInt(1_000_000).MulRaw(params.Ether).BigInt() + tx, err := token.Mint(txOpts, slvr, eth1m) + if err != nil { + return errors.Wrap(err, "mint") + } + + _, err = backend.WaitMined(ctx, tx) + if err != nil { + return errors.Wrap(err, "wait mined") + } + + return nil +} + +// AllowOutboxCalls allows the outbox to call the L1 vault deposit method. +func AllowOutboxCalls(ctx context.Context, network netconf.Network, backends ethbackend.Backends) error { + if network.ID != netconf.Devnet { + return errors.New("onl devnet") + } + + addrs, err := contracts.GetAddresses(ctx, network.ID) + if err != nil { + return errors.Wrap(err, "get addresses") + } + + mockl1, ok := network.Chain(evmchain.IDMockL1) if !ok { - return errors.New("no mock l2") + return errors.New("no mock l1") } mockl1Backend, err := backends.Backend(mockl1.ID) @@ -47,21 +109,35 @@ func Deploy(ctx context.Context, network netconf.Network, backends ethbackend.Ba return errors.Wrap(err, "backend mock l1") } - mockl2Backend, err := backends.Backend(mockl2.ID) + if err := allowCalls(ctx, mockl1Backend, addrs.SolveOutbox); err != nil { + return errors.Wrap(err, "allow calls") + } + + return nil +} + +// allowCalls allows the outbox to call the L1 vault deposit method. +func allowCalls(ctx context.Context, backend *ethbackend.Backend, outboxAddr common.Address) error { + outbox, err := bindings.NewSolveOutbox(outboxAddr, backend) if err != nil { - return errors.Wrap(err, "backend mock l2") + return errors.Wrap(err, "new solve outbox") } - if err := deployToken(ctx, mockl1Backend, l1TokenSalt); err != nil { - return errors.Wrap(err, "deploy l1 token") + txOpts, err := backend.BindOpts(ctx, manager) + if err != nil { + return errors.Wrap(err, "bind opts") } - if err := deployVault(ctx, mockl1Backend, l1VaultSalt, static.L1Token); err != nil { - return errors.Wrap(err, "deploy vault") + vaultDepositID, err := cast.Array4(vaultDeposit.ID[:4]) + if err != nil { + return err } - if err := deployToken(ctx, mockl2Backend, l2TokenSalt); err != nil { - return errors.Wrap(err, "deploy l2 token") + tx, err := outbox.SetAllowedCall(txOpts, static.L1Vault, vaultDepositID, true) + if err != nil { + return errors.Wrap(err, "set allowed call") + } else if _, err := backend.WaitMined(ctx, tx); err != nil { + return errors.Wrap(err, "wait mined") } return nil diff --git a/e2e/solve/devapp/deposit.go b/e2e/solve/devapp/deposit.go index 2c1bb0450..00faa0df3 100644 --- a/e2e/solve/devapp/deposit.go +++ b/e2e/solve/devapp/deposit.go @@ -3,13 +3,16 @@ package devapp import ( "context" "math/big" + "time" "github.com/omni-network/omni/contracts/bindings" "github.com/omni-network/omni/lib/anvil" "github.com/omni-network/omni/lib/contracts" "github.com/omni-network/omni/lib/errors" "github.com/omni-network/omni/lib/ethclient/ethbackend" + "github.com/omni-network/omni/lib/log" "github.com/omni-network/omni/lib/netconf" + "github.com/omni-network/omni/lib/xchain" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" @@ -78,7 +81,107 @@ func IsDeposited(ctx context.Context, backends ethbackend.Backends, req DepositR // assumes balance(onBehalfOf) was zero before deposit request // assumes one deposit per test case onBehalfOf addr - return balance.Cmp(req.Deposit.Amount) != 0, nil + return balance.Cmp(req.Deposit.Amount) == 0, nil +} + +// TestFlow submits deposit requests to the solve inbox and waits for them to be processed. +func TestFlow(ctx context.Context, network netconf.Network, endpoints xchain.RPCEndpoints) error { + backends, err := ethbackend.BackendsFromNetwork(network, endpoints) + if err != nil { + return err + } + + deposits, err := RequestDeposits(ctx, backends) + if err != nil { + return err + } + + timeout, cancel := context.WithTimeout(ctx, time.Minute) + defer cancel() + + // Wait for all deposits to be completed on dest chain by solver/outbox + toCheck := toSet(deposits) + for { + if timeout.Err() != nil { + return errors.New("timeout waiting for deposits") + } + + for deposit := range toCheck { + ok, err := IsDeposited(ctx, backends, deposit) + if err != nil { + return err + } else if ok { + log.Debug(ctx, "Deposit complete", "remaining", len(toCheck)-1) + delete(toCheck, deposit) + } + } + + if len(toCheck) == 0 { + break + } + + time.Sleep(time.Second) + } + + log.Debug(ctx, "All deposits fulfilled") + + // Wait for requests to be claimed by solver + toCheck = toSet(deposits) + for { + if timeout.Err() != nil { + return errors.New("timeout waiting for claims") + } + + const statusClaimed = 6 + + for deposit := range toCheck { + status, err := GetDepositStatus(ctx, backends, deposit) + if err != nil { + return err + } else if status == statusClaimed { + log.Debug(ctx, "Deposit claimed", "remaining", len(toCheck)-1) + delete(toCheck, deposit) + } + } + + if len(toCheck) == 0 { + break + } + + time.Sleep(time.Second) + } + + log.Debug(ctx, "All deposits claimed") + + return nil +} + +func GetDepositStatus(ctx context.Context, backends ethbackend.Backends, deposit DepositReq) (uint8, error) { + app := GetApp() + + backend, err := backends.Backend(app.L2.ChainID) + if err != nil { + return 0, errors.Wrap(err, "backend") + } + + addrs, err := contracts.GetAddresses(ctx, netconf.Devnet) + if err != nil { + return 0, errors.Wrap(err, "get addresses") + } + + inbox, err := bindings.NewSolveInbox(addrs.SolveInbox, backend) + if err != nil { + return 0, errors.Wrap(err, "new mock vault") + } + + callOpts := &bind.CallOpts{Context: ctx} + + req, err := inbox.GetRequest(callOpts, deposit.ID) + if err != nil { + return 0, errors.Wrap(err, "get balance") + } + + return req.Status, nil } // addRandomDepositors adds n random depositors privkeys to the backend. @@ -224,10 +327,19 @@ func parseReqID(inbox bindings.SolveInboxFilterer, logs []*types.Log) ([32]byte, } func packDeposit(args DepositArgs) ([]byte, error) { - data, err := vaultDeposit.Inputs.Pack(args.OnBehalfOf, args.Amount) + calldata, err := vaultABI.Pack("deposit", args.OnBehalfOf, args.Amount) if err != nil { - return nil, errors.Wrap(err, "pack data") + return nil, errors.Wrap(err, "pack deposit call data") + } + + return calldata, nil +} + +func toSet[T comparable](slice []T) map[T]bool { + set := make(map[T]bool) + for _, v := range slice { + set[v] = true } - return data, nil + return set } diff --git a/e2e/solve/devapp/deposit_internal_test.go b/e2e/solve/devapp/deposit_internal_test.go new file mode 100644 index 000000000..c29419cb5 --- /dev/null +++ b/e2e/solve/devapp/deposit_internal_test.go @@ -0,0 +1,27 @@ +package devapp + +import ( + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + + "github.com/stretchr/testify/require" +) + +func TestPackUnpack(t *testing.T) { + t.Parallel() + + dep := DepositArgs{ + OnBehalfOf: common.Address{}, + Amount: big.NewInt(1), + } + + packed, err := packDeposit(dep) + require.NoError(t, err) + + dep2, err := unpackDeposit(packed) + require.NoError(t, err) + + require.Equal(t, dep, dep2) +} diff --git a/e2e/solve/devapp/devapp.go b/e2e/solve/devapp/devapp.go index 84249fd15..b942c1dd9 100644 --- a/e2e/solve/devapp/devapp.go +++ b/e2e/solve/devapp/devapp.go @@ -2,9 +2,12 @@ package devapp import ( "github.com/omni-network/omni/contracts/bindings" + "github.com/omni-network/omni/e2e/app/eoa" + "github.com/omni-network/omni/lib/contracts" "github.com/omni-network/omni/lib/create3" "github.com/omni-network/omni/lib/errors" "github.com/omni-network/omni/lib/evmchain" + "github.com/omni-network/omni/lib/netconf" "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/accounts/abi/bind" @@ -26,7 +29,10 @@ var ( mockL1 = mustChainMeta(evmchain.IDMockL1) mockL2 = mustChainMeta(evmchain.IDMockL2) - // static is the static devnt app instance. + create3Factory = contracts.Create3Factory(netconf.Devnet) + deployer = eoa.MustAddress(netconf.Devnet, eoa.RoleDeployer) + manager = eoa.MustAddress(netconf.Devnet, eoa.RoleManager) + static = App{ L1Vault: create3.Address(create3Factory, l1VaultSalt, deployer), L1Token: create3.Address(create3Factory, l1TokenSalt, deployer), diff --git a/e2e/solve/devapp/target.go b/e2e/solve/devapp/target.go index 8ef8f830a..2e7edd08a 100644 --- a/e2e/solve/devapp/target.go +++ b/e2e/solve/devapp/target.go @@ -1,11 +1,14 @@ package devapp import ( + "bytes" + "context" "math/big" "github.com/omni-network/omni/contracts/bindings" "github.com/omni-network/omni/lib/errors" "github.com/omni-network/omni/lib/evmchain" + "github.com/omni-network/omni/lib/log" solver "github.com/omni-network/omni/solver/types" "github.com/ethereum/go-ethereum/common" @@ -78,8 +81,28 @@ func (a App) Verify(srcChainID uint64, call bindings.SolveCall, deposits []bindi return nil } +func (a App) DebugCall(ctx context.Context, call bindings.SolveCall) error { + args, err := unpackDeposit(call.Data) + if err != nil { + return errors.Wrap(err, "unpack deposit") + } + + if call.Target != a.L1Vault { + return errors.New("unexpected target", "expected", a.L1Vault, "actual", call.Target) + } + + log.Debug(ctx, "MockVault.Deposit", "on_behalf_of", args.OnBehalfOf, "amount", args.Amount, "target", call.Target) + + return nil +} + func unpackDeposit(data []byte) (DepositArgs, error) { - unpacked, err := vaultDeposit.Inputs.Unpack(data) + trimmed := bytes.TrimPrefix(data, vaultDeposit.ID) + if bytes.Equal(trimmed, data) { + return DepositArgs{}, errors.New("data not prefixed with deposit method id") + } + + unpacked, err := vaultDeposit.Inputs.Unpack(trimmed) if err != nil { return DepositArgs{}, errors.Wrap(err, "unpack data") } diff --git a/e2e/test/solve_test.go b/e2e/test/solve_test.go index 81b20df00..fa0455158 100644 --- a/e2e/test/solve_test.go +++ b/e2e/test/solve_test.go @@ -3,12 +3,9 @@ package e2e_test import ( "context" "testing" - "time" "github.com/omni-network/omni/e2e/solve/devapp" "github.com/omni-network/omni/e2e/types" - "github.com/omni-network/omni/lib/ethclient/ethbackend" - "github.com/omni-network/omni/lib/log" "github.com/omni-network/omni/lib/netconf" "github.com/omni-network/omni/lib/xchain" @@ -23,46 +20,7 @@ func TestSolver(t *testing.T) { } maybeTestNetwork(t, skipFunc, func(t *testing.T, network netconf.Network, endpoints xchain.RPCEndpoints) { t.Helper() - ctx := context.Background() - - backends, err := ethbackend.BackendsFromNetwork(network, endpoints) - require.NoError(t, err) - - deposits, err := devapp.RequestDeposits(ctx, backends) + err := devapp.TestFlow(context.Background(), network, endpoints) require.NoError(t, err) - - timeout, cancel := context.WithTimeout(ctx, time.Minute) - defer cancel() - - toCheck := toSet(deposits) - for { - if timeout.Err() != nil { - require.Fail(t, "timeout waiting for deposits") - } - - for deposit := range toCheck { - ok, err := devapp.IsDeposited(ctx, backends, deposit) - require.NoError(t, err) - if ok { - log.Info(ctx, "Deposit complete", "remaining", len(toCheck)-1) - delete(toCheck, deposit) - } - } - - if len(toCheck) == 0 { - return - } - - time.Sleep(time.Second) - } }) } - -func toSet[T comparable](slice []T) map[T]bool { - set := make(map[T]bool) - for _, v := range slice { - set[v] = true - } - - return set -} diff --git a/lib/cast/cast.go b/lib/cast/cast.go index 61e6bb6e3..6d31fc4c0 100644 --- a/lib/cast/cast.go +++ b/lib/cast/cast.go @@ -83,7 +83,7 @@ func Array20[A any](slice []A) ([20]A, error) { return [20]A{}, errors.New("slice length not 20", "len", len(slice)) } -// Array8 casts a slice to an array of length 2. +// Array8 casts a slice to an array of length 8. func Array8[A any](slice []A) ([8]A, error) { if len(slice) == 8 { return [8]A(slice), nil @@ -91,3 +91,12 @@ func Array8[A any](slice []A) ([8]A, error) { return [8]A{}, errors.New("slice length not 8", "len", len(slice)) } + +// Array4 casts a slice to an array of length 4. +func Array4[A any](slice []A) ([4]A, error) { + if len(slice) == 4 { + return [4]A(slice), nil + } + + return [4]A{}, errors.New("slice length not 4", "len", len(slice)) +} diff --git a/lib/umath/umath.go b/lib/umath/umath.go index fea7fa8a0..9723d13e1 100644 --- a/lib/umath/umath.go +++ b/lib/umath/umath.go @@ -12,6 +12,18 @@ import ( "golang.org/x/exp/constraints" ) +// MaxUint256 is the maximum value that can be represented by a uint256. +var MaxUint256 = func() *big.Int { + // Copied from uint256 package. + const twoPow256Sub1 = "115792089237316195423570985008687907853269984665640564039457584007913129639935" + maxUint256, ok := new(big.Int).SetString(twoPow256Sub1, 10) + if !ok { + panic("invalid max uint256") + } + + return maxUint256 +}() + // Subtract returns a - b and true if a >= b, otherwise 0 and false. func Subtract(a, b uint64) (uint64, bool) { if a < b { diff --git a/lib/umath/umath_test.go b/lib/umath/umath_test.go index bae2b17aa..e22caa898 100644 --- a/lib/umath/umath_test.go +++ b/lib/umath/umath_test.go @@ -3,13 +3,25 @@ package umath_test import ( "fmt" "math" + "math/big" "testing" "github.com/omni-network/omni/lib/umath" + "github.com/holiman/uint256" "github.com/stretchr/testify/require" ) +func TestMaxUint256(t *testing.T) { + t.Parallel() + _, overflow := uint256.FromBig(umath.MaxUint256) + require.False(t, overflow, "don't expect overflow") + + maxPlus1 := new(big.Int).Add(umath.MaxUint256, big.NewInt(1)) + _, overflow = uint256.FromBig(maxPlus1) + require.True(t, overflow, "expect overflow") +} + func TestUintTo(t *testing.T) { t.Parallel() tests := []struct { diff --git a/solver/app/app.go b/solver/app/app.go index 285c769f4..2c33f9bd6 100644 --- a/solver/app/app.go +++ b/solver/app/app.go @@ -229,7 +229,7 @@ func startEventStreams( ShouldReject: newShouldRejector(network.ID), Accept: newAcceptor(inboxContracts, backends, solverAddr), Reject: newRejector(inboxContracts, backends, solverAddr), - Fulfill: newFulfiller(network.ID, outboxContracts, backends, solverAddr), + Fulfill: newFulfiller(network.ID, outboxContracts, backends, solverAddr, addrs.SolveOutbox), Claim: newClaimer(inboxContracts, backends, solverAddr), SetCursor: cursorSetter, } @@ -263,7 +263,7 @@ func streamEventsForever( req := xchain.EventLogsReq{ ChainID: chainID, - Height: from, + Height: from, // Note the previous height is re-processed (idempotency FTW) ConfLevel: confLevel, FilterAddress: inboxAddr, FilterTopics: allEventTopics, diff --git a/solver/app/helpers.go b/solver/app/helpers.go index e27d04dfb..29f25e248 100644 --- a/solver/app/helpers.go +++ b/solver/app/helpers.go @@ -36,5 +36,5 @@ func detectContractChains(ctx context.Context, network netconf.Network, backends // fmtReqID returns the least-significant 7 hex chars of the provided request ID. // ReqIDs are monotonically incrementing numbers, not hashes. func fmtReqID(reqID [32]byte) string { - return hex.EncodeToString(reqID[:])[:7] + return hex.EncodeToString(reqID[:])[64-7:] } diff --git a/solver/app/deps.go b/solver/app/procdeps.go similarity index 60% rename from solver/app/deps.go rename to solver/app/procdeps.go index ceae0a018..c63a17c05 100644 --- a/solver/app/deps.go +++ b/solver/app/procdeps.go @@ -2,12 +2,16 @@ package app import ( "context" + "math/big" + "strings" "github.com/omni-network/omni/contracts/bindings" + "github.com/omni-network/omni/lib/cast" "github.com/omni-network/omni/lib/errors" "github.com/omni-network/omni/lib/ethclient/ethbackend" "github.com/omni-network/omni/lib/log" "github.com/omni-network/omni/lib/netconf" + "github.com/omni-network/omni/lib/umath" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" @@ -65,25 +69,27 @@ func newFulfiller( network netconf.ID, outboxContracts map[uint64]*bindings.SolveOutbox, backends ethbackend.Backends, - solverAddr common.Address, -) func(ctx context.Context, chainID uint64, req bindings.SolveRequest) error { - return func(ctx context.Context, chainID uint64, req bindings.SolveRequest) error { - outbox, ok := outboxContracts[chainID] + solverAddr, outboxAddr common.Address, +) func(ctx context.Context, srcChainID uint64, req bindings.SolveRequest) error { + return func(ctx context.Context, srcChainID uint64, req bindings.SolveRequest) error { + destChainID := req.Call.DestChainId // Fulfilling happens on destination chain + outbox, ok := outboxContracts[destChainID] if !ok { return errors.New("unknown chain") } - backend, err := backends.Backend(chainID) + backend, err := backends.Backend(destChainID) if err != nil { return err } + callOpts := &bind.CallOpts{Context: ctx} txOpts, err := backend.BindOpts(ctx, solverAddr) if err != nil { return err } - if ok, err := outbox.DidFulfill(&bind.CallOpts{Context: ctx}, req.Id, chainID, req.Call); err != nil { + if ok, err := outbox.DidFulfill(callOpts, req.Id, srcChainID, req.Call); err != nil { return errors.Wrap(err, "did fulfill") } else if ok { log.Info(ctx, "Skipping already fulfilled request", "req_id", req.Id) @@ -100,17 +106,132 @@ func newFulfiller( return errors.Wrap(err, "get token prereqs") } - tx, err := outbox.Fulfill(txOpts, req.Id, chainID, req.Call, prereqs) + for _, prereq := range prereqs { + if err := approveOutboxSpend(ctx, prereq, backend, solverAddr, outboxAddr); err != nil { + return errors.Wrap(err, "approve outbox spend") + } + + if err := checkAllowedCall(ctx, outbox, req.Call); err != nil { + return errors.Wrap(err, "check allowed call") + } + } + + if err := target.DebugCall(ctx, req.Call); err != nil { + return errors.Wrap(err, "debug call") + } + + // xcall fee + fee, err := outbox.FulfillFee(callOpts, srcChainID) + if err != nil { + return errors.Wrap(err, "get fulfill fee") + } + + txOpts.Value = fee + tx, err := outbox.Fulfill(txOpts, req.Id, srcChainID, req.Call, prereqs) if err != nil { - return errors.Wrap(err, "fulfill request") + return errors.Wrap(err, "fulfill request", "custom", detectCustomError(err)) } else if _, err := backend.WaitMined(ctx, tx); err != nil { return errors.Wrap(err, "wait mined") } + if ok, err := outbox.DidFulfill(callOpts, req.Id, srcChainID, req.Call); err != nil { + return errors.Wrap(err, "did fulfill") + } else if !ok { + return errors.New("fulfill failed [BUG]") + } + return nil } } +func detectCustomError(custom error) string { + contracts := map[string]*bind.MetaData{ + "inbox": bindings.SolveInboxMetaData, + "outbox": bindings.SolveOutboxMetaData, + "mock_vault": bindings.MockVaultMetaData, + "mock_token": bindings.MockTokenMetaData, + } + + for name, contract := range contracts { + abi, err := contract.GetAbi() + if err != nil { + return "BUG" + } + for n, e := range abi.Errors { + if strings.Contains(custom.Error(), e.ID.Hex()[:10]) { + return name + "::" + n + } + } + } + + return "unknown" +} + +func checkAllowedCall(ctx context.Context, outbox *bindings.SolveOutbox, call bindings.SolveCall) error { + callOpts := &bind.CallOpts{Context: ctx} + + if len(call.Data) < 4 { + return errors.New("invalid call data") + } + + callMethodID, err := cast.Array4(call.Data[:4]) + if err != nil { + return err + } + + allowed, err := outbox.AllowedCalls(callOpts, call.Target, callMethodID) + if err != nil { + return errors.Wrap(err, "get allowed calls") + } else if !allowed { + return errors.New("call not allowed") + } + + return nil +} + +func approveOutboxSpend(ctx context.Context, prereq bindings.SolveTokenPrereq, backend *ethbackend.Backend, solverAddr, outboxAddr common.Address) error { + // TODO(kevin): make erc20 bindings and use here + token, err := bindings.NewMockToken(prereq.Token, backend) + if err != nil { + return errors.Wrap(err, "new token") + } + + isApproved := func() (bool, error) { + allowance, err := token.Allowance(&bind.CallOpts{Context: ctx}, solverAddr, outboxAddr) + if err != nil { + return false, errors.Wrap(err, "get allowance") + } + + return new(big.Int).Sub(allowance, prereq.Amount).Sign() >= 0, nil + } + + if approved, err := isApproved(); err != nil { + return err + } else if approved { + return nil + } + + txOpts, err := backend.BindOpts(ctx, solverAddr) + if err != nil { + return err + } + + tx, err := token.Approve(txOpts, outboxAddr, umath.MaxUint256) + if err != nil { + return errors.Wrap(err, "approve token") + } else if _, err := backend.WaitMined(ctx, tx); err != nil { + return errors.Wrap(err, "wait mined") + } + + if approved, err := isApproved(); err != nil { + return err + } else if !approved { + return errors.New("approve failed") + } + + return nil +} + func newRejector( inboxContracts map[uint64]*bindings.SolveInbox, backends ethbackend.Backends, diff --git a/solver/types/target.go b/solver/types/target.go index 86e78b137..155bf7752 100644 --- a/solver/types/target.go +++ b/solver/types/target.go @@ -1,6 +1,8 @@ package types import ( + "context" + "github.com/omni-network/omni/contracts/bindings" "github.com/ethereum/go-ethereum/common" @@ -20,4 +22,7 @@ type Target interface { // Verify returns an error if the call should not be fulfilled. // TODO(corver): Return reject reason. Verify(srcChainID uint64, call bindings.SolveCall, deposits []bindings.SolveDeposit) error + + // DebugCall logs the call for debugging purposes. + DebugCall(ctx context.Context, call bindings.SolveCall) error }