From ec5f1ad988bb874a38c13fc71319e49c7db4a9bb Mon Sep 17 00:00:00 2001 From: Garry Sharp <> Date: Mon, 28 Jul 2025 21:39:12 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Update=20recipe=20dependency=20and?= =?UTF-8?q?=20enhance=20fee=20plugin=20functionality=20with=20magic=20cons?= =?UTF-8?q?tants?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- go.mod | 6 +-- go.sum | 14 +------ plugin/fees/policy.go | 18 ++++++++- plugin/fees/transaction.go | 63 +++++++++++++++++------------- storage/postgres/schema/schema.sql | 42 +++++++++++++++++++- 5 files changed, 97 insertions(+), 46 deletions(-) diff --git a/go.mod b/go.mod index a7a37fc..10f0af6 100644 --- a/go.mod +++ b/go.mod @@ -17,11 +17,10 @@ require ( github.com/redis/go-redis/v9 v9.8.0 github.com/sirupsen/logrus v1.9.3 github.com/spf13/viper v1.20.1 - github.com/stretchr/testify v1.10.0 github.com/vultisig/commondata v0.0.0-20250710214228-61d9ed8f7778 github.com/vultisig/mobile-tss-lib v0.0.0-20250316003201-2e7e570a4a74 github.com/vultisig/recipes v0.0.0-20250729120802-9b1d07f8262a - github.com/vultisig/verifier v0.0.0-20250723150319-a51c59a884bf + github.com/vultisig/verifier v0.0.0-20250728211124-309f39dbdb80 github.com/vultisig/vultiserver v0.0.0-20250715212748-4b23f9849e4b golang.org/x/sync v0.14.0 google.golang.org/protobuf v1.36.6 @@ -137,6 +136,7 @@ require ( github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible // indirect github.com/spf13/cobra v1.8.1 // indirect github.com/stretchr/objx v0.5.2 // indirect + github.com/stretchr/testify v1.10.0 // indirect github.com/supranational/blst v0.3.14 // indirect github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d // indirect github.com/tendermint/go-amino v0.16.0 // indirect @@ -177,7 +177,7 @@ require ( github.com/otiai10/primes v0.4.0 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/robfig/cron/v3 v3.0.1 + github.com/robfig/cron/v3 v3.0.1 // indirect github.com/sagikazarmark/locafero v0.7.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.12.0 // indirect diff --git a/go.sum b/go.sum index 3c0ef31..2554364 100644 --- a/go.sum +++ b/go.sum @@ -757,20 +757,10 @@ github.com/vultisig/go-wrappers v0.0.0-20250403041248-86911e8aa33f h1:124Xlloih1 github.com/vultisig/go-wrappers v0.0.0-20250403041248-86911e8aa33f/go.mod h1:UfGCxUQW08kiwxyNBiHwXe+ePPuBmHVVS+BS51aU/Jg= github.com/vultisig/mobile-tss-lib v0.0.0-20250316003201-2e7e570a4a74 h1:goqwk4nQ/NEVIb3OPP9SUx7/u9ZfsUIcd5fIN/e4DVU= github.com/vultisig/mobile-tss-lib v0.0.0-20250316003201-2e7e570a4a74/go.mod h1:nOykk4nOy1L3yXtLSlYvVsgizBnCQ3tR2N5uwGPdvaM= -github.com/vultisig/recipes v0.0.0-20250723142134-153c7f486070 h1:AcE6x2dxkl/4P/TxaMJ70pa2BcZ3+FJWnVmlPVAq4Dk= -github.com/vultisig/recipes v0.0.0-20250723142134-153c7f486070/go.mod h1:30NOW5y2BnMCmEYFeVls3NhxkbMvuv7BNyRQfoFhGIM= -github.com/vultisig/recipes v0.0.0-20250724111913-675e7fdcbc24 h1:NKkcuAtSSQfaYTQ0Fo96mnTsTDAoTh+XrbrUHLEliNY= -github.com/vultisig/recipes v0.0.0-20250724111913-675e7fdcbc24/go.mod h1:Ot3lrUnnSw67Hep+MelclVPgNLDxJP01Ezixw/1RYRE= -github.com/vultisig/recipes v0.0.0-20250724145933-37c12287503c h1:PFaQfDNMSBymgwu+6Jr9+wYF/tFpv3jk97i8VE2+bSY= -github.com/vultisig/recipes v0.0.0-20250724145933-37c12287503c/go.mod h1:Ot3lrUnnSw67Hep+MelclVPgNLDxJP01Ezixw/1RYRE= -github.com/vultisig/recipes v0.0.0-20250724150617-ab960a120ce8 h1:aMVowo/fdQGizJf7lFDzh07t7GmKvLyddVOWeFm7dkw= -github.com/vultisig/recipes v0.0.0-20250724150617-ab960a120ce8/go.mod h1:Ot3lrUnnSw67Hep+MelclVPgNLDxJP01Ezixw/1RYRE= -github.com/vultisig/recipes v0.0.0-20250724151024-afb75caf8884 h1:ObxhyUAEQYfmuqH2mnvw/fidEmaDPIEbxdp3oIpM9WY= -github.com/vultisig/recipes v0.0.0-20250724151024-afb75caf8884/go.mod h1:Ot3lrUnnSw67Hep+MelclVPgNLDxJP01Ezixw/1RYRE= github.com/vultisig/recipes v0.0.0-20250729120802-9b1d07f8262a h1:KoAwytLj092KNgWHh0a5tcupsSm5HtentT0yUbDGWFQ= github.com/vultisig/recipes v0.0.0-20250729120802-9b1d07f8262a/go.mod h1:Ot3lrUnnSw67Hep+MelclVPgNLDxJP01Ezixw/1RYRE= -github.com/vultisig/verifier v0.0.0-20250723150319-a51c59a884bf h1:3VhxFQR0oJEieaajNDvKlOGBDyTBbSMJSxow3zwUlmw= -github.com/vultisig/verifier v0.0.0-20250723150319-a51c59a884bf/go.mod h1:9f3yGSZWKZeXn30mU+/brufkbkLoGlFmNLKOpRbQgeI= +github.com/vultisig/verifier v0.0.0-20250728211124-309f39dbdb80 h1:aOgViLVMBV1E8VicCh2g9pky0I+NXLlWNvzDm5c1HBk= +github.com/vultisig/verifier v0.0.0-20250728211124-309f39dbdb80/go.mod h1:eK+KyWZSgiB/gzDJjsDKx+ry7gtJadplzpMNF4gvkKk= github.com/vultisig/vultiserver v0.0.0-20250715212748-4b23f9849e4b h1:Ed2DOWo8fA0KG6e36rzUmGpxcOQjmWTbxWyvUbI5by8= github.com/vultisig/vultiserver v0.0.0-20250715212748-4b23f9849e4b/go.mod h1:HwP2IgW6Mcu/gX8paFuKvfibrGE9UmPgkOFTub6dskM= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= diff --git a/plugin/fees/policy.go b/plugin/fees/policy.go index cc6a3fa..8f92713 100644 --- a/plugin/fees/policy.go +++ b/plugin/fees/policy.go @@ -26,6 +26,12 @@ func (fp *FeePlugin) ValidatePluginPolicy(policyDoc vtypes.PluginPolicy) error { } func (fp *FeePlugin) GetRecipeSpecification() (*rtypes.RecipeSchema, error) { + + cfg, err := plugin.RecipeConfiguration(map[string]any{}) + if err != nil { + return nil, fmt.Errorf("failed to build pb recipe config: %w", err) + } + return &rtypes.RecipeSchema{ Version: 1, // Schema version ScheduleVersion: 1, // Schedule specification version @@ -38,13 +44,13 @@ func (fp *FeePlugin) GetRecipeSpecification() (*rtypes.RecipeSchema, error) { ChainId: "ethereum", ProtocolId: "usdc", FunctionId: "transfer", - Full: "ethereum.usdc.transfer", + Full: "ethereum.erc20.transfer", }, ParameterCapabilities: []*rtypes.ParameterConstraintCapability{ { ParameterName: "recipient", SupportedTypes: []rtypes.ConstraintType{ - rtypes.ConstraintType_CONSTRAINT_TYPE_FIXED, + rtypes.ConstraintType_CONSTRAINT_TYPE_MAGIC_CONSTANT, }, Required: true, }, @@ -55,6 +61,13 @@ func (fp *FeePlugin) GetRecipeSpecification() (*rtypes.RecipeSchema, error) { }, Required: true, }, + { + ParameterName: "token", + SupportedTypes: []rtypes.ConstraintType{ + rtypes.ConstraintType_CONSTRAINT_TYPE_FIXED, + }, + Required: true, + }, }, Required: true, }, @@ -63,5 +76,6 @@ func (fp *FeePlugin) GetRecipeSpecification() (*rtypes.RecipeSchema, error) { MinVultisigVersion: 1, SupportedChains: []string{"ethereum"}, }, + Configuration: cfg, }, nil } diff --git a/plugin/fees/transaction.go b/plugin/fees/transaction.go index cfdf72d..26cde70 100644 --- a/plugin/fees/transaction.go +++ b/plugin/fees/transaction.go @@ -6,6 +6,7 @@ import ( "encoding/base64" "fmt" "math/big" + "strconv" "strings" "github.com/google/uuid" @@ -14,10 +15,11 @@ import ( "github.com/vultisig/recipes/chain" "github.com/vultisig/recipes/engine" reth "github.com/vultisig/recipes/ethereum" + resolver "github.com/vultisig/recipes/resolver" + rtypes "github.com/vultisig/recipes/types" "github.com/vultisig/verifier/address" vcommon "github.com/vultisig/verifier/common" - "github.com/vultisig/verifier/tx_indexer/pkg/storage" vtypes "github.com/vultisig/verifier/types" gcommon "github.com/ethereum/go-ethereum/common" @@ -55,30 +57,50 @@ func (fp *FeePlugin) ProposeTransactions(policy vtypes.PluginPolicy) ([]vtypes.P return nil, fmt.Errorf("failed to get recipe from policy: %v", err) } - chain := vcommon.Ethereum txs := []vtypes.PluginKeysignRequest{} - + var magicConstantRecipientValue rtypes.MagicConstant = rtypes.MagicConstant_UNSPECIFIED + var token string // This should only return one rule, but in case there are more/fewer rules, we'll loop through them all and error if it's the case. for _, rule := range recipe.Rules { // This section of code goes through the rules in the fee policy. It looks for the recipient of the fee collection policy and extracts it. If other data is found throws an error as they're unsupported rules. var recipient string // The address specified in the fee policy. switch rule.Resource { - case "ethereum.usdc.transfer": + case "ethereum.erc20.transfer": for _, constraint := range rule.ParameterConstraints { if constraint.ParameterName == "recipient" { + if constraint.Constraint.Type != rtypes.ConstraintType_CONSTRAINT_TYPE_MAGIC_CONSTANT { + return nil, fmt.Errorf("recipient constraint is not a magic constant") + } + iv, err := strconv.ParseInt(constraint.Constraint.GetFixedValue(), 10, 64) + if err != nil { + return nil, fmt.Errorf("failed to parse fixed value: %v", err) + } + magicConstantRecipientValue = rtypes.MagicConstant(iv) + } + if constraint.ParameterName == "token" { if constraint.Constraint.Type != rtypes.ConstraintType_CONSTRAINT_TYPE_FIXED { - return nil, fmt.Errorf("recipient constraint is not a fixed value") + return nil, fmt.Errorf("token constraint is not a fixed value") } + token = constraint.Constraint.GetFixedValue() } - fixedValue := constraint.Constraint.GetValue().(*rtypes.Constraint_FixedValue) - recipient = fixedValue.FixedValue } default: return nil, fmt.Errorf("unsupported rule: %v", rule.Id) } - if recipient == "" { - return nil, fmt.Errorf("recipient is not set in policy") + + if magicConstantRecipientValue != rtypes.MagicConstant_VULTISIG_TREASURY { + return nil, fmt.Errorf("recipient constraint is not a treasury magic constant") + } + + treasuryResolver := resolver.NewDefaultTreasuryResolver() + recipient, _, err := treasuryResolver.Resolve(magicConstantRecipientValue, "ethereum", "usdc") + if err != nil { + return nil, fmt.Errorf("failed to resolve treasury address: %v", err) + } + + if gcommon.HexToAddress(token) != gcommon.HexToAddress(usdc.Address) { + return nil, fmt.Errorf("token address does not match usdc address") } // Here we call the verifier api to get a list of fees that have the same public key as the signed policy document. @@ -105,19 +127,6 @@ func (fp *FeePlugin) ProposeTransactions(policy vtypes.PluginPolicy) ([]vtypes.P } txHashToSign := etypes.LatestSignerForChainID(fp.config.ChainId).Hash(etypes.NewTx(txData)) - txToTrack, e := fp.txIndexerService.CreateTx(ctx, storage.CreateTxDto{ - PluginID: policy.PluginID, - PolicyID: policy.ID, - ChainID: chain, - TokenID: usdc.Address, - FromPublicKey: policy.PublicKey, - ToPublicKey: recipient, - ProposedTxHex: txHex, - }) - if e != nil { - return nil, fmt.Errorf("error creating tx indexed transaction: %w", e) - } - msgHash := sha256.Sum256(txHashToSign.Bytes()) signRequest := vtypes.PluginKeysignRequest{ @@ -125,8 +134,8 @@ func (fp *FeePlugin) ProposeTransactions(policy vtypes.PluginPolicy) ([]vtypes.P PublicKey: policy.PublicKey, Messages: []vtypes.KeysignMessage{ { - TxIndexerID: txToTrack.ID.String(), Message: base64.StdEncoding.EncodeToString(txHashToSign.Bytes()), + RawMessage: txHex, Chain: vcommon.Ethereum, Hash: base64.StdEncoding.EncodeToString(msgHash[:]), HashFunction: vtypes.HashFunction_SHA256, @@ -157,11 +166,8 @@ func (fp *FeePlugin) initSign( if len(req.Messages) != 1 { return fmt.Errorf("multiple messages in key sign request, expected 1") } - txId, err := uuid.Parse(req.Messages[0].TxIndexerID) - if err != nil { - return fmt.Errorf("failed to parse tx indexer id: %w", err) - } - err = fp.db.SetFeeRunSent(ctx, runId, txId) + + err := fp.db.SetFeeRunSent(ctx, runId, uuid.Nil) // TODO this will be replaced imminently in an upcoming PR if err != nil { return fmt.Errorf("failed to update fee run: %w", err) } @@ -240,6 +246,7 @@ func (fp *FeePlugin) ValidateProposedTransactions(policy vtypes.PluginPolicy, tx func (fp *FeePlugin) SigningComplete(ctx context.Context, signature tss.KeysignResponse, signRequest vtypes.PluginKeysignRequest, policy vtypes.PluginPolicy) error { // Broadcast the signed transaction to the Ethereum network + tx, err := fp.eth.Send( ctx, gcommon.FromHex(signRequest.Transaction), diff --git a/storage/postgres/schema/schema.sql b/storage/postgres/schema/schema.sql index f8ead9e..1c52ce3 100644 --- a/storage/postgres/schema/schema.sql +++ b/storage/postgres/schema/schema.sql @@ -17,6 +17,39 @@ CREATE TYPE "tx_indexer_status_onchain" AS ENUM ( 'FAIL' ); +CREATE FUNCTION "prevent_insert_if_policy_deleted"() RETURNS "trigger" + LANGUAGE "plpgsql" + AS $$ +BEGIN + IF NEW.deleted = true THEN + RAISE EXCEPTION 'Cannot insert a deleted policy'; + END IF; + RETURN NEW; +END; +$$; + +CREATE FUNCTION "prevent_update_if_policy_deleted"() RETURNS "trigger" + LANGUAGE "plpgsql" + AS $$ +BEGIN + IF OLD.deleted = true THEN + RAISE EXCEPTION 'Cannot update a deleted policy'; + END IF; + RETURN NEW; +END; +$$; + +CREATE FUNCTION "set_policy_inactive_on_delete"() RETURNS "trigger" + LANGUAGE "plpgsql" + AS $$ +BEGIN + IF NEW.deleted = true THEN + NEW.active := false; + END IF; + RETURN NEW; +END; +$$; + CREATE FUNCTION "update_updated_at_column"() RETURNS "trigger" LANGUAGE "plpgsql" AS $$ @@ -66,7 +99,8 @@ CREATE TABLE "plugin_policies" ( "recipe" "text" NOT NULL, "active" boolean DEFAULT true NOT NULL, "created_at" timestamp with time zone DEFAULT "now"() NOT NULL, - "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL + "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL, + "deleted" boolean DEFAULT false NOT NULL ); CREATE TABLE "scheduler" ( @@ -125,6 +159,12 @@ CREATE INDEX "idx_tx_indexer_key" ON "tx_indexer" USING "btree" ("chain_id", "pl CREATE INDEX "idx_tx_indexer_status_onchain_lost" ON "tx_indexer" USING "btree" ("status_onchain", "lost"); +CREATE TRIGGER "trg_prevent_insert_if_policy_deleted" BEFORE INSERT ON "plugin_policies" FOR EACH ROW EXECUTE FUNCTION "public"."prevent_insert_if_policy_deleted"(); + +CREATE TRIGGER "trg_prevent_update_if_policy_deleted" BEFORE UPDATE ON "plugin_policies" FOR EACH ROW WHEN (("old"."deleted" = true)) EXECUTE FUNCTION "public"."prevent_update_if_policy_deleted"(); + +CREATE TRIGGER "trg_set_policy_inactive_on_delete" BEFORE INSERT OR UPDATE ON "plugin_policies" FOR EACH ROW WHEN (("new"."deleted" = true)) EXECUTE FUNCTION "public"."set_policy_inactive_on_delete"(); + CREATE TRIGGER "update_fee_run_updated_at" BEFORE UPDATE ON "fee_run" FOR EACH ROW EXECUTE FUNCTION "public"."update_updated_at_column"(); ALTER TABLE ONLY "fee"