From 15eeafdc10fb42f264d9d69f136290e4c99d8cdf Mon Sep 17 00:00:00 2001 From: Garry Sharp <> Date: Fri, 15 Aug 2025 20:23:54 -0500 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fee=20config=20bug=20fix=20and?= =?UTF-8?q?=20target=20added=20to=20tx=20fee=20validations?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- plugin/fees/config.go | 43 ++++++++---- plugin/fees/transaction.go | 137 +++++++++++++++++++------------------ 2 files changed, 99 insertions(+), 81 deletions(-) diff --git a/plugin/fees/config.go b/plugin/fees/config.go index d4a6218..f7914b0 100644 --- a/plugin/fees/config.go +++ b/plugin/fees/config.go @@ -11,14 +11,13 @@ import ( // These are properties and parameters specific to the fee plugin config. They should be distinct from system/core config type FeeConfig struct { - Type string `mapstructure:"type"` - Version string `mapstructure:"version"` - MaxFeeAmount uint64 `mapstructure:"max_fee_amount"` // Policies that are created/submitted which do not have this amount will be rejected. - UsdcAddress string `mapstructure:"usdc_address"` // The address of the USDC token on the Ethereum blockchain. - VerifierToken string `mapstructure:"verifier_token"` // The token to use for the verifier API. - chainId uint64 `mapstructure:"chain_id"` // The chain ID of the Ethereum blockchain. - ChainId *big.Int - EthProvider string `mapstructure:"eth_provider"` // The Ethereum provider to use for the fee plugin. + Type string `mapstructure:"type"` + Version string `mapstructure:"version"` + MaxFeeAmount uint64 `mapstructure:"max_fee_amount"` // Policies that are created/submitted which do not have this amount will be rejected. + UsdcAddress string `mapstructure:"usdc_address"` // The address of the USDC token on the Ethereum blockchain. + VerifierToken string `mapstructure:"verifier_token"` // The token to use for the verifier API. + ChainId *big.Int // The chain ID as a big.Int (initialized from ChainIdRaw). + EthProvider string `mapstructure:"eth_provider"` // The Ethereum provider to use for the fee plugin. Jobs struct { Load struct { MaxConcurrentJobs uint64 `mapstructure:"max_concurrent_jobs"` //How many consecutive tasks can take place @@ -34,6 +33,12 @@ type FeeConfig struct { MaxConcurrentJobs uint64 `mapstructure:"max_concurrent_jobs"` } `mapstructure:"post"` } + DryRun bool `mapstructure:"dry_run"` +} + +type FeeConfigFileWrapper struct { + FeeConfig `mapstructure:",squash"` + ChainIdRaw uint64 `mapstructure:"chain_id"` } type ConfigOption func(*FeeConfig) error @@ -52,6 +57,7 @@ func withDefaults(c *FeeConfig) { c.Jobs.Load.Cronexpr = "@every 2m" c.Jobs.Transact.Cronexpr = "0 12 * * 5" c.Jobs.Post.Cronexpr = "@every 5m" + c.DryRun = false } func WithMaxFeeAmount(maxFeeAmount uint64) ConfigOption { @@ -100,9 +106,16 @@ func WithCronexpr(load, transact, post string) ConfigOption { } } -func WithFileConfig(basePath string) ConfigOption { +func WithDryRun(dryRun bool) ConfigOption { return func(c *FeeConfig) error { + c.DryRun = dryRun + return nil + } +} + +func WithFileConfig(basePath string) ConfigOption { + return func(c *FeeConfig) error { v := viper.New() v.SetConfigName("fee") @@ -122,11 +135,15 @@ func WithFileConfig(basePath string) ConfigOption { return fmt.Errorf("failed to read config: %w", err) } - if err := v.Unmarshal(c); err != nil { + var wrappedConfig FeeConfigFileWrapper + if err := v.Unmarshal(&wrappedConfig); err != nil { return fmt.Errorf("failed to unmarshal config: %w", err) } - c.ChainId = big.NewInt(0).SetUint64(c.chainId) + // Copy all values from the wrapped config to the original config + *c = wrappedConfig.FeeConfig + + c.ChainId = big.NewInt(0).SetUint64(wrappedConfig.ChainIdRaw) return nil } } @@ -149,8 +166,8 @@ func NewFeeConfig(fns ...ConfigOption) (*FeeConfig, error) { return c, errors.New("verifier_token is required") } - if c.ChainId == nil { - return c, errors.New("chain_id is required") + if c.ChainId == nil || c.ChainId.Uint64() == 0 { + return c, errors.New("chain_id is required and must not be 0") } if c.EthProvider == "" { diff --git a/plugin/fees/transaction.go b/plugin/fees/transaction.go index 39cc1ca..29eedbb 100644 --- a/plugin/fees/transaction.go +++ b/plugin/fees/transaction.go @@ -63,88 +63,89 @@ func (fp *FeePlugin) proposeTransactions(ctx context.Context, policy vtypes.Plug 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.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 len(recipe.Rules) != 1 { + return nil, fmt.Errorf("expected 1 rule, got %d", len(recipe.Rules)) + } + rule := recipe.Rules[0] + + resourceName := "ethereum.erc20.transfer" + if rule.Resource != resourceName { + return nil, fmt.Errorf("rule resource expected to be %s", resourceName) + } + + 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) } - default: - return nil, fmt.Errorf("unsupported rule: %v", rule.Id) + magicConstantRecipientValue = rtypes.MagicConstant(iv) } + } - if magicConstantRecipientValue != rtypes.MagicConstant_VULTISIG_TREASURY { - return nil, fmt.Errorf("recipient constraint is not a treasury magic constant") - } + 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) - } + 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") - } + token := rule.Target.GetAddress() - amount := run.TotalAmount + if gcommon.HexToAddress(token) != gcommon.HexToAddress(usdc.Address) { + return nil, fmt.Errorf("token address does not match usdc address") + } - tx, err := fp.eth.MakeAnyTransfer(ctx, - gcommon.HexToAddress(ethAddress), - gcommon.HexToAddress(recipient), - gcommon.HexToAddress(usdc.Address), - big.NewInt(int64(amount))) - if err != nil { - return nil, fmt.Errorf("failed to generate unsigned transaction: %w", err) - } + amount := run.TotalAmount - txHex := hexutil.Encode(tx) + tx, err := fp.eth.MakeAnyTransfer(ctx, + gcommon.HexToAddress(ethAddress), + gcommon.HexToAddress(recipient), + gcommon.HexToAddress(usdc.Address), + big.NewInt(int64(amount))) + if err != nil { + return nil, fmt.Errorf("failed to generate unsigned transaction: %w", err) + } - txData, e := reth.DecodeUnsignedPayload(tx) - if e != nil { - return nil, fmt.Errorf("ethereum.DecodeUnsignedPayload: %w", e) - } - txHashToSign := etypes.LatestSignerForChainID(fp.config.ChainId).Hash(etypes.NewTx(txData)) - - msgHash := sha256.Sum256(txHashToSign.Bytes()) - - signRequest := vtypes.PluginKeysignRequest{ - KeysignRequest: vtypes.KeysignRequest{ - PublicKey: policy.PublicKey, - Messages: []vtypes.KeysignMessage{ - { - Message: base64.StdEncoding.EncodeToString(txHashToSign.Bytes()), - RawMessage: txHex, - Chain: vcommon.Ethereum, - Hash: base64.StdEncoding.EncodeToString(msgHash[:]), - HashFunction: vtypes.HashFunction_SHA256, - }, + txHex := hexutil.Encode(tx) + + txData, e := reth.DecodeUnsignedPayload(tx) + if e != nil { + return nil, fmt.Errorf("ethereum.DecodeUnsignedPayload: %w", e) + } + + txHashToSign := etypes.LatestSignerForChainID(fp.config.ChainId).Hash(etypes.NewTx(txData)) + msgHash := sha256.Sum256(txHashToSign.Bytes()) + signRequest := vtypes.PluginKeysignRequest{ + KeysignRequest: vtypes.KeysignRequest{ + PublicKey: policy.PublicKey, + Messages: []vtypes.KeysignMessage{ + { + Message: base64.StdEncoding.EncodeToString(txHashToSign.Bytes()), + RawMessage: txHex, + Chain: vcommon.Ethereum, + Hash: base64.StdEncoding.EncodeToString(msgHash[:]), + HashFunction: vtypes.HashFunction_SHA256, }, - SessionID: "", - HexEncryptionKey: "", - PolicyID: policy.ID, - PluginID: policy.PluginID.String(), }, - Transaction: txHex, - } - - txs = append(txs, signRequest) + SessionID: "", + HexEncryptionKey: "", + PolicyID: policy.ID, + PluginID: policy.PluginID.String(), + }, + Transaction: txHex, } + txs = append(txs, signRequest) + return txs, nil }