A trading firm has a bot that needs to sign transactions. The private key lives in a secure signing service, and the bot has permission to request signatures. The key never appears in the bot’s configuration file.
That’s a good start. Then someone compromises the bot process.
The attacker may never see the private key. They can still use the bot’s access to ask for signatures. If the signer accepts their requests, those signatures can move money or operate contracts just as effectively as requests from the legitimate bot.
When I work with trading firms and institutions, I want to understand what the software can ask that key to do, which checks happen independently, and what happens if someone takes control of the process.
I’ll use an example system with three separate signing roles: one for treasury transactions, one that posts prices, and one that can pause a trading contract. The architecture, limits and incidents below are illustrative. They are a way to examine the controls; they don’t describe a real firm’s setup.
Start before the signing request
The bot’s code builds transactions and sends them to a signing service. The service holds the key, checks requests against its policy and returns a signature for an allowed request. A separate broadcaster sends the signed transaction to the chain.
If an attacker controls the running process, they can change the requests it makes and may be able to use its credentials until those credentials are revoked or expire. They may also read data available to the process, interfere with the price inputs or stop the bot altogether. Protecting the key from export still matters; it limits key theft. But we need to examine the signing access the process already has.
I would start with the people and systems that can change the bot: repository maintainers, deployment approvers, CI credentials, container registry access and anyone who can change runtime configuration or connect to the host. A carefully reviewed repository won’t help if someone can replace the production image or alter the running process without the same controls.
For this example, I want protected code review, a controlled deployment identity, an approved image digest and recorded configuration changes. I would verify those against repository and deployment settings, logs and the image actually running. Then I’d try a safe unauthorized deployment in a test environment. A written instruction saying “only approved code runs” remains a procedure until the deployment path enforces it.
What does the signer actually see?
The signer is a separate boundary only if the compromised bot cannot change its policy or bypass it. If the bot can call the underlying key service directly, or ask another service to sign arbitrary data with the same key, the policy in front of it may not help.
I’d map every identity that can use the key: application credentials, service accounts, administrative users, backup integrations and emergency access. I’d check who can change those permissions and the signer policy. The key service should accept signing requests only through the intended policy enforcement path, and policy changes should require separate authorization. I’d test the direct and alternate routes too.
There is an important detail here. A key management service can protect a private key and control which identity may call a signing API, yet the operation it signs may be a digest with no readable transaction context. The team needs a trusted policy layer that receives and independently decodes the full request, checks its meaning, and signs the exact bytes it checked. If the signer only sees an opaque hash, it cannot itself establish the recipient, amount or contract function. AWS KMS documentation describes how an asymmetric private key is used through the service; it does not imply that KMS understands a team’s DeFi transaction policy.
I’d verify the complete chain, including transaction types and message-signing methods. Typed messages, permits and contract-specific signatures can authorize actions without looking like an ordinary token transfer. The signer needs to reject unsupported formats and bind approvals to the exact chain, account, destination and payload. Test malformed and undecodable requests as well as normal ones.
A treasury signer can put money at risk
Suppose the treasury gives the bot a signing role to trade up to 1,000 USDC at a time through an approved router. The WETH bought by the swap must return to the treasury’s trading wallet. The wallet holds 50,000 USDC.
If the compromised bot can request arbitrary signatures from an unrestricted treasury key, it may be able to transfer assets out, grant token allowances or sign other authorizations. If it is limited to the router but the output recipient is left open, it can pay for a swap and send the WETH elsewhere. That’s the gap we walked through in the wallet-policy article.
For the example, I’d have the independent signer enforce the chain, wallet, router, single-swap method, input and output assets, maximum input and destination wallet. It should bind the approved minimum output and deadline to the exact request. An independent quote or price check is needed if the bot itself supplies the minimum: a compromised bot could otherwise set a near-zero minimum and accept a terrible trade. The contract-side wallet can enforce corresponding limits where its design supports them; inspect the actual implementation and all modules and execution paths.
A 1,000 USDC limit per request still allows repeated requests. I’d separately enforce an aggregate limit and reset period, with atomic accounting for concurrent requests and pending signatures. For example, a 5,000 USDC daily budget can limit approved exposure if every signing route shares that accounting. I’d check the wallet’s existing allowances, separately authorize a bounded approval to the router, and test signed permit paths. A spending cap does not recover funds already moved or constrain an unreviewed route.
I’d verify the active policy and on-chain wallet configuration, then submit safe positive and negative cases through the actual supported test path: wrong receiver, wrong router, weak minimum output, oversized approval, repeated requests and two simultaneous requests competing for the remaining budget. Record the decision and policy version. The exact balance at risk depends on the wallet, allowances, permissions and other execution paths; a test model alone cannot establish the live account’s protection.
A price signer can change other people’s decisions
Now consider a separate key whose job is to post a price to a protocol’s oracle contract. It holds no treasury role and cannot directly transfer the treasury’s USDC in this example. A compromised bot may still post a bad price if the signer and contract accept it.
Imagine a lending contract uses the posted price to value collateral. An inflated price could let a borrower take too much credit; an understated price could trigger liquidations. The loss depends on the protocol’s positions, timing and safeguards. Even without a successful exploit, an attacker can stop updates and disrupt systems that rely on fresh data.
I’d restrict this signer to the oracle contract and its specific update function, including the intended asset and network. The signer should check a trustworthy, independently obtained reference, units and decimals, freshness, and a defined deviation bound before signing. The contract should independently enforce the authorized reporter, positive values, timestamp or sequence rules and its own bounds where feasible. Consumers also need freshness checks and a defined response to stale or suspect prices. A deviation limit alone can be walked gradually over multiple updates; compare against independent references and cumulative movement, and account for real market gaps.
For a Chainlink feed used as a reference, I’d identify the exact feed and check its latestRoundData() answer and updatedAt, along with the application’s freshness and validity requirements. Chainlink documents those fields in its data feed API reference. A stale or unavailable reference should trigger the documented fail-safe, rather than silently accepting whatever the bot proposes.
I’d test a valid update, wrong asset, stale input, replayed sequence, extreme price and a series of individually small moves. I’d also test what the consumer does when prices stop arriving. Signer-side checks, contract rules and consumer behavior need to be checked separately: inspect the active policies and code, reproduce the boundary cases in a representative environment, and compare the results with the values actually configured on-chain.
A pause-only signer can still stop the business
The third key can call pause() on a trading contract. In our example it has no permission to unpause, upgrade the contract, transfer funds or grant roles. A different, separately controlled authority can resume operations after investigation.
A compromised bot using this key could still pause trading at the worst moment. Orders may fail, positions may remain open and the team may be unable to rebalance. What happens to withdrawals, cancellations and liquidations depends on which functions the contract actually pauses. I would check each path in the code and test it; a function named pause does not tell me its full effect.
I’d confirm the pause role on the live contract, inspect who can grant it, and test that the key cannot unpause, change roles or call other privileged functions. For a contract using OpenZeppelin’s role controls, hasRole and the role’s admin relationship are relevant starting points; the access-control documentation explains how role administration works. Custom contracts and proxy upgrades may behave differently. Read the active implementation and current state before drawing a conclusion.
Removing access takes more than one switch
If we suspect the bot is compromised, I’d want a rehearsed response with named owners and a backup. The team should stop the process and isolate the affected workload, disable its signing credentials and any alternate paths, and preserve logs and request records for investigation. Check that new requests are rejected; credential revocation can take time to propagate, and other active sessions or identities may still work.
Then address the on-chain permission. An independent authority may need to remove the old signer from the wallet or revoke its oracle or pause role, and verify the resulting state on-chain. The required administrator, multisig threshold, timelock and gas need to be known beforehand. If revocation is delayed, the old role can remain usable until the change takes effect. If the bot can change its own access or the admin is compromised too, the recovery plan needs a different trusted route.
Stopping the signer also doesn’t erase signatures already issued. A signed transaction may still be broadcast, and an off-chain signature may remain valid until its nonce, deadline or contract authorization makes it unusable. Depending on the system, the team may need to invalidate nonces, cancel pending operations, revoke allowances or change the contract’s authorized signer. Check the actual design and race conditions; a cancellation may lose the race to an existing transaction. Record confirmed transactions, remaining balances and active permissions after the response.
Recovery has to restore the service safely. Build from a trusted version, investigate how access was obtained, issue new credentials and a new signer where appropriate, grant only the required role, and test the new path before resuming. For an oracle, confirm how stale prices and dependent positions are handled while updates are stopped. For a pause role, make sure the independent team can still respond if the replacement isn’t ready.
What I’d want to see before trusting it
A runbook is useful when people know who to call and how to respond. For each important restriction, I’d also want to check the active control: the signer policy, the key-service access configuration, the deployed contract and its role state, plus a recorded test that tries to cross the boundary.
The three roles have very different consequences. Treasury access can expose assets. A price reporter can affect valuations and liquidations. A pause-only account may interrupt trading. Each deserves its own key, permissions, monitoring and recovery plan, sized to what that role can actually do.
If you’d like help reviewing a trading bot, oracle or signer setup, we can discuss it through my fractional security advisory service.