Skip to content

Conversation

@CPerezz
Copy link
Contributor

@CPerezz CPerezz commented May 12, 2025

🏭 Contract Deployment State Bloat

This scenario deploys contracts that are exactly 24kB in size (EIP-170 limit) to maximize state growth while minimizing gas cost.

How it Works

  1. Generates a contract with exactly 24,576 bytes of runtime code
  2. Deploys the contract using CREATE with a salt that makes the bytecode unique
  3. Uses batch-based deployment:
    • Calculates how many contracts fit in one block based on gas limits
    • Sends a batch of transactions that fit within the block gas limit
    • Waits for a new block to be mined
    • Repeats the process ("bombards" the RPC after each block)
  4. Each deployment adds:
    • 24,576 bytes of runtime code
    • Account trie node
    • Total state growth: ~24.7kB per deployment

⛽ Gas Cost Breakdown

  • 32,000 gas for CREATE
  • 20,000 gas for new account
  • 200 gas per byte for code deposit (24,576 bytes)
  • Total: 4,967,200 gas per deployment

Batch Strategy

The scenario automatically calculates how many contracts can fit in one block:

  • Default block gas limit: 30,000,000 gas
  • Gas per contract: 4,967,200 gas
  • Contracts per batch: ~6 contracts per block

This ensures optimal utilization of block space while maintaining predictable transaction inclusion patterns.

🚀 Usage

Build

go build -o bin/spamoor cmd/spamoor/main.go

Run

./bin/spamoor --privkey <PRIVATE_KEY> --rpchost http://localhost:8545 contract-deploy [flags]

Key Flags

  • --max-transactions - Total number of contracts to deploy (0 = infinite, default: 0)
  • --max-wallets - Max child wallets to use (0 = root wallet only, default: 0)
  • --basefee - Base fee per gas in gwei (default: 10)
  • --tipfee - Tip fee per gas in gwei (default: 2)

Example with Anvil node

./bin/spamoor --privkey ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 \
  --rpchost http://localhost:8545 contract-deploy \
  --max-transactions 0

CPerezz added 9 commits May 7, 2025 16:45
Includes the scenarios contemplated within statebloat as well as any
extra data about them such as gas cost metrics.
This scenario deploys contracts that are exactly 24kB in size (EIP-170 limit) to maximize state growth while minimizing gas cost.

1. Generates a contract with exactly 24,576 bytes of runtime code
2. Deploys the contract using CREATE
3. Each deployment adds:
   - 24,576 bytes of runtime code
   - Account trie node
   - Total state growth: ~24.7kB per deployment

- 32,000 gas for CREATE
- 20,000 gas for new account
- 200 gas per byte for code deposit (24,576 bytes)
- Total: 4,967,200 gas per deployment
- Remove redundant throughput flag and consolidate to contracts-per-tx
- Remove count flag and related code for cleaner interface
- Update wallet count calculation to use contracts-per-tx
- Keep only two deployment rate control methods:
  - contracts-per-tx: direct control of contracts per transaction
  - gas-per-block: calculate contracts based on target gas

The scenario now runs indefinitely until stopped, with cleaner and more
focused configuration options.
- Add validation to ensure contract deployments don't exceed block gas limit
- Check both gas-per-block and contracts-per-tx against block gas limit
- Add clear error messages when gas limits are exceeded
- Move validation to Run() function to access context
- Introduce multiple test cases for contract deployment: using contracts per block, gas per block, and handling invalid configurations.
- Update config opts to replace `contracts-per-tx` with `contracts-per-block` for better clarity.
- Add error handling for scenarios where neither gas per block nor contracts per block is set.
- Refactor gas fee parameters to align with EIP-1559 standards.
- Initialize wallet pool in the contract deployment test.
@pk910
Copy link
Member

pk910 commented May 12, 2025

Heya @CPerezz,
Thanks for your contributions :)

I've reviewed the document, and it looks like most of the cheap state growth scenarios are already supported by the existing spamoor scenarios:

  1. contract deployments with deploytx scenario (supply bytecode as --bytecodes / --bytecodes-file)
  2. add delegates via setcodetx scenario
  3. eoatx scenario with --random-target
  4. should be possible with a slight extension of setcodetx
  5. that one would be new :)

The Reversible state bloat attack vectors sound like they can be fully achieved with the gasburnertx / geastx scenarios.
But needs some custom evm asm code to do the actual bloat technique

@CPerezz
Copy link
Contributor Author

CPerezz commented May 25, 2025

Hey @pk910 apologies for the late reply.

Mainly the deployment contract scenario was quite different from what I need. Yes, we both deploy contracts but I needed those to be the same ABI but different bytecode by using a random salt on deploymet such that the bytecode storage for clients gets bloated too (identical bytecodes aren't duplicated ofc).

Also, this scenario grants quite some logging utilities needed later to use the wallets to bloat the KV stores with new addresses getting tokens.

I'm completelly OK if you prefer to not merge this as is super similar.
Nevertheless, I'll leave all the scenarios here and you can tell me if you want me to upstream any of them!

@CPerezz CPerezz force-pushed the add/state_growth_worst_cases branch from 6c63e86 to f92a90e Compare May 30, 2025 06:06
@CPerezz CPerezz force-pushed the add/state_growth_worst_cases branch 2 times, most recently from 715d413 to 13604ed Compare June 18, 2025 10:11
CPerezz added 13 commits June 18, 2025 17:21
- Introduced setup and teardown for contract deployment tests.
- Updated scenario options to include a maximum transactions limit. St we can test for a single tx at a time and shortening testing time.
…to max (EIP170)

- Introduced multiple dummy functions in the StateBloatToken contract to artificially inflate bytecode size.
- Updated ABI and binary files to reflect changes in the contract structure.
- Updated the contract deployment scenario to log deployed contract addresses and gas used.
- Added functionality to write deployed addresses to a JSON file for easier tracking.
- Removed gas-per-block validation and adjusted wallet count logic.
- Improved README with instructions for running against a local Anvil node without Go tests.
…ction tracking

- Introduced a batch deployment strategy that calculates the number of contracts fitting within the block gas limit.
- Added detailed tracking for deployed contracts, including gas used and bytecode size, with structured loggin.
- Improved nonce management and transaction retry logic.
… logging

- Added functionality to save a mapping of private keys to contract addresses in a deployments.json file after each contract deployment.
- Refactored the final summary logging to focus on the deployments.json file, removing the previous detailed contract saving logic.
- imporved logging to provide insights into the total number of deployers and contracts processed.
…mprove transaction handling

- Removed unused imports and redundant fields.
- Simplified transaction processing by releasing locks earlier.
- Improved logging for transaction sending and contract deployment confirmation.
- Cleaned up nonce management by directly fetching the nonce from the client.
…djustment

- Added functionality to dynamically adjust transaction fees based on current network conditions.
- Implemented retry logic for transaction sending with exponential backoff for base fee errors.
…s_limit

- Introduced BlockDeploymentStats struct to track deployment statistics per block, including contract count, total gas used, and total bytecode size.
- Implemented real-time block monitoring for logging deployment summaries.
- Updated contract bytecode size calculations to reflect actual deployed bytecode.
- Adjusted transaction processing intervals for improved efficiency.
@CPerezz CPerezz force-pushed the add/state_growth_worst_cases branch from 6df98c4 to 139b18e Compare June 18, 2025 15:23
@CPerezz CPerezz changed the title Add/state growth worst cases for BloatNet (feat) Introduce contract-deployment bloating scenario Jun 18, 2025
Signed-off-by: CPerezz <37264926+CPerezz@users.noreply.github.com>
CPerezz added 2 commits June 19, 2025 00:27
…based rate limiting

- Set MaxPending default to 100 to fix scenario hanging at startup (was 0, preventing any transactions)
- Remove hardcoded 12-second block time assumption that caused deployments only every 2 blocks
- Implement proper block-based rate limiting that waits for actual block changes
- Set rate limit to 2x expected throughput for better network utilization
- Send transactions at expected throughput rate per block instead of arbitrary delays
…nt scenario

- Replace incorrect import 'scenariotypes' with 'scenario' package
- Update ScenarioDescriptor to use correct scenario.Descriptor type
- Fix Init method signature to match scenario.Scenario interface
- Update wallet private key access to use GetWallet().GetPrivateKey()
- Remove unused Config() method not part of the interface
- Fix walletPool references in Init method
@CPerezz
Copy link
Contributor Author

CPerezz commented Jun 18, 2025

Seems I screwed up a bit when splitting into multiple PRs. With these two commits the scenario should now be ready.

@pk910 pk910 mentioned this pull request Jun 19, 2025
4 tasks
Comment on lines +347 to +407
// startBlockMonitor starts a background goroutine that monitors for new blocks
// and logs block deployment summaries immediately when blocks are mined
func (s *Scenario) startBlockMonitor(ctx context.Context) {
monitorCtx, cancel := context.WithCancel(ctx)
s.blockMonitorCancel = cancel
s.blockMonitorDone = make(chan struct{})

go func() {
defer close(s.blockMonitorDone)

client := s.walletPool.GetClient(spamoor.SelectClientByIndex, 0, s.options.ClientGroup)
if client == nil {
s.logger.Warn("No client available for block monitoring")
return
}

ethClient := client.GetEthClient()
ticker := time.NewTicker(2 * time.Second) // Poll every 2 seconds
defer ticker.Stop()

for {
select {
case <-monitorCtx.Done():
return
case <-ticker.C:
// Get current block number
latestBlock, err := ethClient.BlockByNumber(monitorCtx, nil)
if err != nil {
s.logger.WithError(err).Debug("Failed to get latest block for monitoring")
continue
}

currentBlockNumber := latestBlock.Number().Uint64()

// Log any completed blocks that haven't been logged yet
s.blockStatsMutex.Lock()
for bn := s.lastLoggedBlock + 1; bn < currentBlockNumber; bn++ {
if stats, exists := s.blockStats[bn]; exists && stats.ContractCount > 0 {
avgGasPerByte := float64(stats.TotalGasUsed) / float64(max(stats.TotalBytecodeSize, 1))

s.contractsMutex.Lock()
totalContracts := len(s.deployedContracts)
s.contractsMutex.Unlock()

s.logger.WithFields(logrus.Fields{
"block_number": bn,
"contracts_deployed": stats.ContractCount,
"total_gas_used": stats.TotalGasUsed,
"total_bytecode_size": stats.TotalBytecodeSize,
"avg_gas_per_byte": fmt.Sprintf("%.2f", avgGasPerByte),
"total_contracts": totalContracts,
}).Info("Block deployment summary")

s.lastLoggedBlock = bn
}
}
s.blockStatsMutex.Unlock()
}
}
}()
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is basically what the TxPool logic does and shouldn't be duplicated.
you can get the receipts for each deployment in the SubmitTransactionOptions.OnConfirm callback and do the statistics summary there,
or subscribe to block updates via txPool.SubscribeToBlockUpdates, which will give you a per block event with all transactions / receipts that are relevant for the current scenario.

Comment on lines +143 to +153
func (s *Scenario) getChainID(ctx context.Context) (*big.Int, error) {
s.chainIDOnce.Do(func() {
client := s.walletPool.GetClient(spamoor.SelectClientByIndex, 0, s.options.ClientGroup)
if client == nil {
s.chainIDError = fmt.Errorf("no client available for chain ID")
return
}
s.chainID, s.chainIDError = client.GetChainId(ctx)
})
return s.chainID, s.chainIDError
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the ChainId is cached and can be accessed via s.walletPool.GetChainId()

Comment on lines +155 to +240
// waitForPendingTxSlot waits until we have capacity for another transaction
func (s *Scenario) waitForPendingTxSlot(ctx context.Context) {
for {
s.pendingTxsMutex.RLock()
count := len(s.pendingTxs)
s.pendingTxsMutex.RUnlock()

if count < int(s.options.MaxPending) {
return
}

// Check and clean up confirmed transactions
s.processPendingTransactions(ctx)
time.Sleep(1 * time.Second)
}
}

// processPendingTransactions checks for transaction confirmations and updates state
func (s *Scenario) processPendingTransactions(ctx context.Context) {
s.pendingTxsMutex.Lock()

client := s.walletPool.GetClient(spamoor.SelectClientByIndex, 0, s.options.ClientGroup)
if client == nil {
s.pendingTxsMutex.Unlock()
return
}

ethClient := client.GetEthClient()
var confirmedTxs []common.Hash
var timedOutTxs []common.Hash
var successfulDeployments []struct {
ContractAddress common.Address
PrivateKey *ecdsa.PrivateKey
Receipt *types.Receipt
TxHash common.Hash
}

for txHash, pendingTx := range s.pendingTxs {
// Check if transaction is too old (1 minute timeout)
if time.Since(pendingTx.Timestamp) > 1*time.Minute {
s.logger.Warnf("Transaction %s timed out after 1 minute, removing from pending", txHash.Hex())
timedOutTxs = append(timedOutTxs, txHash)
continue
}

receipt, err := ethClient.TransactionReceipt(ctx, txHash)
if err != nil {
// Transaction still pending or error retrieving receipt
continue
}

confirmedTxs = append(confirmedTxs, txHash)

// Process successful deployment
if receipt.Status == 1 && receipt.ContractAddress != (common.Address{}) {
successfulDeployments = append(successfulDeployments, struct {
ContractAddress common.Address
PrivateKey *ecdsa.PrivateKey
Receipt *types.Receipt
TxHash common.Hash
}{
ContractAddress: receipt.ContractAddress,
PrivateKey: pendingTx.PrivateKey,
Receipt: receipt,
TxHash: txHash,
})
}
}

// Remove confirmed transactions from pending map
for _, txHash := range confirmedTxs {
delete(s.pendingTxs, txHash)
}

// Remove timed out transactions from pending map
for _, txHash := range timedOutTxs {
delete(s.pendingTxs, txHash)
}

s.pendingTxsMutex.Unlock()

// Process successful deployments after releasing the lock
for _, deployment := range successfulDeployments {
s.recordDeployedContract(deployment.ContractAddress, deployment.PrivateKey, deployment.Receipt, deployment.TxHash)
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this duplicates the transaction tracking logic and shouldn't be necessary.
You'll get the receipt in the SendTransactionOptions.OnConfirm callback, so you should just call s.recordDeployedContract from there.

PrivateKey: fmt.Sprintf("0x%x", crypto.FromECDSA(privateKey)),
}

s.deployedContracts = append(s.deployedContracts, deployment)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels like a memory leak, as the deployedContracts slice can grow over hours and cause OOM kills at some point.
Consider adding those entries in a append-style file.
maybe as yaml array, so you can easily append lines without reading & re-writing the whole file:

- {"ContractAddress": "0x...", "PrivateKey": "0x..."}
- {"ContractAddress": "0x...", "PrivateKey": "0x..."}
...

Comment on lines +284 to +302
// Create or update current block stats (removed the old logging logic)
if s.blockStats[blockNumber] == nil {
s.blockStats[blockNumber] = &BlockDeploymentStats{
BlockNumber: blockNumber,
}
s.logger.WithField("block_number", blockNumber).Debug("Created new block stats")
}

blockStat := s.blockStats[blockNumber]
blockStat.ContractCount++
blockStat.TotalGasUsed += receipt.GasUsed
blockStat.TotalBytecodeSize += bytecodeSize

s.logger.WithFields(logrus.Fields{
"block_number": blockNumber,
"contracts_in_block": blockStat.ContractCount,
"gas_used": blockStat.TotalGasUsed,
"bytecode_size": blockStat.TotalBytecodeSize,
}).Debug("Updated block stats")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't be necessary, subscribe to block notifications as mentioned above if you want per block stats.

Comment on lines +448 to +540
// Calculate rate limiting based on block gas limit if max-transactions is 0
var maxTxsPerBlock uint64
var useRateLimiting bool

if s.options.MaxTransactions == 0 {
blockGasLimit := currentBlock.GasLimit()
// TODO: This should be a constant.
estimatedGasPerContract := uint64(4949468) // Updated estimate based on contract size reduction
expectedThroughput := blockGasLimit / estimatedGasPerContract
maxTxsPerBlock = expectedThroughput * 2 // Set rate limit to 2x expected throughput
useRateLimiting = true

s.logger.Infof("Rate limiting enabled: block gas limit %d, gas per contract %d, expected throughput %d, rate limit %d txs/block",
blockGasLimit, estimatedGasPerContract, expectedThroughput, maxTxsPerBlock)
}

txIdxCounter := uint64(0)
totalTxCount := atomic.Uint64{}
blockTxCount := uint64(0)
lastBlockNumber := currentBlock.Number().Uint64()

for {
// Check if we've reached max transactions (if set)
if s.options.MaxTransactions > 0 && txIdxCounter >= s.options.MaxTransactions {
s.logger.Infof("reached maximum number of transactions (%d)", s.options.MaxTransactions)
break
}

// Rate limiting logic
if useRateLimiting {
// If we've sent expected throughput transactions, wait for next block
if blockTxCount >= maxTxsPerBlock/2 { // Send throughput amount, not rate limit amount
s.logger.Infof("Sent %d txs, waiting for next block", blockTxCount)

// Wait for block number to change
for {
currentBlock, err := ethClient.BlockByNumber(ctx, nil)
if err == nil && currentBlock.Number().Uint64() > lastBlockNumber {
lastBlockNumber = currentBlock.Number().Uint64()
blockTxCount = 0
break
}
time.Sleep(100 * time.Millisecond)
}
}
}

// Wait for available slot
s.waitForPendingTxSlot(ctx)

// Send a single transaction
err := s.sendTransaction(ctx, txIdxCounter)
if err != nil {
s.logger.Warnf("failed to send transaction %d: %v", txIdxCounter, err)
time.Sleep(1 * time.Second)
continue
}

txIdxCounter++
totalTxCount.Add(1)
blockTxCount++

// Process pending transactions periodically with 1 second intervals
if txIdxCounter%10 == 0 {
s.processPendingTransactions(ctx)

s.contractsMutex.Lock()
contractCount := len(s.deployedContracts)
s.contractsMutex.Unlock()

s.logger.Infof("Progress: sent %d txs, deployed %d contracts", txIdxCounter, contractCount)
}

// Small delay to prevent overwhelming the RPC
time.Sleep(100 * time.Millisecond)
}

// Wait for all pending transactions to complete with 1 second intervals
s.logger.Info("Waiting for remaining transactions to complete...")
for {
s.processPendingTransactions(ctx)

s.pendingTxsMutex.RLock()
pendingCount := len(s.pendingTxs)
s.pendingTxsMutex.RUnlock()

if pendingCount == 0 {
break
}

s.logger.Infof("Waiting for %d pending transactions...", pendingCount)
time.Sleep(1 * time.Second) // Changed from 2 seconds to 1 second
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this duplicates the scenario.RunTransactionScenario helper, that is shared across almost all scenarios to avoid duplicating the core transaction count / throughput & pending limitations.

it should be something like this:

err := scenario.RunTransactionScenario(ctx, scenario.TransactionScenarioOptions{
		TotalCount: s.options.MaxTransactions,
		Throughput: maxTxsPerBlock,
		MaxPending: maxPending,
		Timeout:    timeout,
		WalletPool: s.walletPool,
		Logger:     s.logger,
		ProcessNextTxFn: func(ctx context.Context, txIdx uint64, onComplete func()) (func(), error) {
		     tx, err := s.sendTransaction(ctx, txIdxCounter, onComplete) // ensure onComplete gets called when the tx completes (either an error before/on submission or confirmation)
		     return func() {
				if err != nil {
					logger.Warnf("could not send transaction: %v", err)
				} else if s.options.LogTxs {
					logger.Infof("sent tx #%6d: %v", txIdx+1, tx.Hash().String())
				} else {
					logger.Debugf("sent tx #%6d: %v", txIdx+1, tx.Hash().String())
				}
			}, err
		},
})

Comment on lines +580 to +602
for attempt := 0; attempt < maxRetries; attempt++ {
err := s.attemptTransaction(ctx, txIdx, attempt)
if err == nil {
return nil
}

// Check if it's a base fee error
if strings.Contains(err.Error(), "max fee per gas less than block base fee") {
s.logger.Warnf("Transaction %d base fee too low, adjusting fees and retrying (attempt %d/%d)",
txIdx, attempt+1, maxRetries)

// Update fees based on current network conditions
if updateErr := s.updateDynamicFees(ctx); updateErr != nil {
s.logger.Warnf("Failed to update dynamic fees: %v", updateErr)
}

time.Sleep(time.Duration(attempt+1) * 500 * time.Millisecond) // Exponential backoff
continue
}

// For other errors, return immediately
return err
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this gas increasing retry loop shouldn't be necessary. Send transactions with a static base fee. You might get a empty block if the base fee is too high, but that's fine, as otherwise the base fee grows exponentially and you can't afford the fee over long term even with nearly unlimited funds.

Comment on lines +608 to +642
func (s *Scenario) updateDynamicFees(ctx context.Context) error {
client := s.walletPool.GetClient(spamoor.SelectClientByIndex, 0, s.options.ClientGroup)
if client == nil {
return fmt.Errorf("no client available")
}

ethClient := client.GetEthClient()

// Get the latest block to check current base fee
latestBlock, err := ethClient.BlockByNumber(ctx, nil)
if err != nil {
return fmt.Errorf("failed to get latest block: %w", err)
}

if latestBlock.BaseFee() != nil {
// Convert base fee from wei to gwei
currentBaseFeeGwei := new(big.Int).Div(latestBlock.BaseFee(), big.NewInt(1000000000))

newBaseFeeGwei := new(big.Int).Add(currentBaseFeeGwei, big.NewInt(100))

s.options.BaseFee = newBaseFeeGwei.Uint64()

// Also increase tip fee slightly to ensure competitive priority
if s.options.TipFee+1 > 3 {
s.options.TipFee = s.options.TipFee + 1
} else {
s.options.TipFee = 2 // Minimum 3 gwei tip
}

s.logger.Infof("Updated dynamic fees - Base fee: %d gwei, Tip fee: %d gwei (network base fee: %s gwei)",
s.options.BaseFee, s.options.TipFee, currentBaseFeeGwei.String())
}

return nil
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the block base fee is tracked in the TxPool and can be requested via TxPool.GetCurrentBaseFee / TxPool.GetCurrentBaseFeeWithInit

Comment on lines +664 to +705
addr := crypto.PubkeyToAddress(wallet.GetWallet().GetPrivateKey().PublicKey)
nonce, err := client.GetEthClient().PendingNonceAt(ctx, addr)
if err != nil {
return fmt.Errorf("failed to get nonce for %s: %w", addr.Hex(), err)
}

// Create transaction auth
auth, err := bind.NewKeyedTransactorWithChainID(wallet.GetWallet().GetPrivateKey(), chainID)
if err != nil {
return fmt.Errorf("failed to create auth: %w", err)
}

auth.Nonce = big.NewInt(int64(nonce))
auth.Value = big.NewInt(0)
auth.GasLimit = 5200000 // Fixed gas limit for contract deployment

// Set EIP-1559 fee parameters
if s.options.BaseFee > 0 {
auth.GasFeeCap = new(big.Int).Mul(big.NewInt(int64(s.options.BaseFee)), big.NewInt(1000000000))
}
if s.options.TipFee > 0 {
auth.GasTipCap = new(big.Int).Mul(big.NewInt(int64(s.options.TipFee)), big.NewInt(1000000000))
}

// Generate random salt for unique contract
salt := make([]byte, 32)
_, err = rand.Read(salt)
if err != nil {
return fmt.Errorf("failed to generate salt: %w", err)
}
saltInt := new(big.Int).SetBytes(salt)

// Deploy the contract
ethClient := client.GetEthClient()
if ethClient == nil {
return fmt.Errorf("failed to get eth client")
}

_, tx, _, err := contract.DeployContract(auth, ethClient, saltInt)
if err != nil {
return fmt.Errorf("failed to deploy contract: %w", err)
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nonce tracking needs to be done within the local wallet, otherwise you'll get duplicate transactions as previous transactions are still being gossiped or otherwise processed.
the spamoor.Wallet provides appropriate methods to build transactions with a thread safe tracked nonce:

feeCap, tipCap, err := s.walletPool.GetTxPool().GetSuggestedFees(client, s.options.BaseFee, s.options.TipFee)
if err != nil {
		return nil, client, err
}

// Generate random salt for unique contract
salt := make([]byte, 32)
_, err = rand.Read(salt)
if err != nil {
  return fmt.Errorf("failed to generate salt: %w", err)
}
saltInt := new(big.Int).SetBytes(salt)

tx, err := wallet.BuildBoundTx(ctx, &txbuilder.TxMetadata{
		GasFeeCap: uint256.MustFromBig(feeCap),
		GasTipCap: uint256.MustFromBig(tipCap),
		Gas:       2000000,
		Value:     uint256.NewInt(0),
}, func(transactOpts *bind.TransactOpts) (*types.Transaction, error) {
		_, deployTx, _, err := contract.DeployContract(transactOpts, client.GetEthClient(), saltInt)
		return deployTx, err
})

@@ -0,0 +1 @@
[{"inputs":[{"internalType":"uint256","name":"_salt","type":"uint256"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"spender","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"from","type":"address"},{"indexed":true,"internalType":"address","name":"to","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"}],"name":"Transfer","type":"event"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"}],"name":"allowance","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"}],"name":"approve","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"balanceOf","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"decimals","outputs":[{"internalType":"uint8","name":"","type":"uint8"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"dummy1","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy10","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy11","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy12","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy13","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy14","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy15","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy16","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy17","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy18","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy19","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy2","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy20","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy21","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy22","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy23","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy24","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy25","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy26","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy27","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy28","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy29","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy3","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy30","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy31","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy32","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy33","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy34","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy35","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy36","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy37","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy38","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy39","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy4","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy40","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy41","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy42","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy43","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy44","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy45","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy46","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy47","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy48","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy49","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy5","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy50","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy51","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy52","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy53","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy54","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy55","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy56","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy57","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy58","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy59","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy6","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy60","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy61","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy62","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy63","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy64","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy65","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy7","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy8","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy9","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"name","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"salt","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"symbol","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"totalSupply","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"}],"name":"transfer","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"}],"name":"transferFrom","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"}],"name":"transferFrom1","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"}],"name":"transferFrom10","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"}],"name":"transferFrom11","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"}],"name":"transferFrom12","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"}],"name":"transferFrom13","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"}],"name":"transferFrom14","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"}],"name":"transferFrom15","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"}],"name":"transferFrom16","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"}],"name":"transferFrom17","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"}],"name":"transferFrom18","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"}],"name":"transferFrom19","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"}],"name":"transferFrom2","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"}],"name":"transferFrom3","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"}],"name":"transferFrom4","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"}],"name":"transferFrom5","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"}],"name":"transferFrom6","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"}],"name":"transferFrom7","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"}],"name":"transferFrom8","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"}],"name":"transferFrom9","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"}] No newline at end of file
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please exclude the .abi & .bin from the repository and copy/amend the compile.sh from another scenario.
that way all contracts are built the same way, which is way easier to maintain.

@pk910
Copy link
Member

pk910 commented Jul 14, 2025

Heya @CPerezz,
I've taken another look at the PR. There is still a lot of code duplicated in a less optimal way, which makes it hard to maintain this scenario.
Could you take a look at those review comments? :)
It's basically what I already tried to fix in #72, but as of your feedback that refactored version doesn't work properly?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants