<!-- canonical: https://0xsimao.com/findings/crestal-network-signature-replay-blueprintcore-lose -->

# Signature Replay attack possible on `updateWorkerDeploymentConfigWithSig()` in Blueprintcore.sol which leads to users lose the funds

Medium · Sherlock · AI agent · 11th March 2025

Finding M-3 of the Crestal Network competition.

- Protocol: https://audits.sherlock.xyz/contests/755
- Codebase: https://github.com/0xsimao/2025-03-crestal-network/tree/27a3c28155702b3a68f29347efedffb048010e33
- Source: https://github.com/sherlock-audit/2025-03-crestal-network-judging/issues/391

---

### Summary

The lack of replay protection in the `updateWorkerDeploymentConfigWithSig` function will cause a significant loss of funds for users as a malicious actor will replay a signed transaction to repeatedly transfer funds from the deployment owner to the fee collection wallet. The protocol didn't have the functionality to refund these funds to the respective users if this issue occurs. So anyway user is gonna lose their fund.

### Root Cause

In `BlueprintCore.sol` at the [`updateWorkerDeploymentConfigWithSig`](https://github.com/sherlock-audit/2025-03-crestal-network/blob/main/crestal-omni-contracts/src/BlueprintCore.sol#L672) function, the function verifies a signature using `getRequestDeploymentDigest` but does not include a nonce, timestamp, or chain ID in the signed message. This allows a valid signature to be reused indefinitely, triggering multiple calls to `updateWorkerDeploymentConfigCommon` and its `payWithERC20` payment logic.

```solidity
function updateWorkerDeploymentConfigWithSig(
    address tokenAddress,
    bytes32 projectId,
    bytes32 requestID,
    string memory updatedBase64Config,
    bytes memory signature
) public {
    bytes32 digest = getRequestDeploymentDigest(projectId, updatedBase64Config, "app.crestal.network");
    address signerAddr = getSignerAddress(digest, signature);
    updateWorkerDeploymentConfigCommon(tokenAddress, signerAddr, projectId, requestID, updatedBase64Config);
}
```
### Internal Pre-conditions

1. The `updateWorkerDeploymentConfigWithSig` function remains public and unchanged in the deployed contract.
2. A user (deployment owner) has approved the `BlueprintCore.sol` contract to spend their ERC-20 tokens via `approve` on the token contract.
3. The user has signed a valid message (with `projectId`, `updatedBase64Config`, "app.crestal.network") and submitted it to update a deployment configuration for a `requestID` with `status` not equal to `Init` or `Issued`.
4. The `paymentOpCostMp[tokenAddress][UPDATE_AGENT_OP]` returns a non-zero `cost`.



### External Pre-conditions

1. The Base blockchain allows transaction replay if the signature remains valid, which is standard behavior unless mitigated.
2. The ERC-20 token contract at `tokenAddress` supports `safeTransferFrom` and doesn't prevent replay.



### Attack Path

1. A user (deployment owner) signs a message to update a deployment configuration (projectId, requestID, updatedBase64Config) and submits it via `updateWorkerDeploymentConfigWithSig`, paying $token to `feeCollectionWalletAddress` via `payWithERC20`.
2. The transaction succeeds, updating the configuration and emitting `UpdateDeploymentConfig`, with the signature recorded on-chain.
3. A malicious actor captures the signature and replays the transaction by calling `updateWorkerDeploymentConfigWithSig` with the same parameters (tokenAddress, projectId, requestID, updatedBase64Config, signature).
4. Each replay re-executes `updateWorkerDeploymentConfigCommon`, transferring another $token from the user to `feeCollectionWalletAddress` (if funds/allowance remain) and resetting status to `Pickup` if it was `GeneratedProof`, repeatable until the user's funds are drained or allowance is revoked.



### Impact

The user suffers an approximate loss of $token per replay. If replayed indefinitely (e.g., 20 times), the loss could reach 20x more, potentially draining 100% of approved funds. The attacker gains no direct funds but indirectly benefits `feeCollectionWalletAddress`, incurring only gas costs per replay.
The protocol didn't have the functionality to refund this token to the respective users if this issue occurs. So anyway user is gonna lose their fund.



### PoC

_No response_

### Mitigation

Add a nonce to the signed message and track it per user:

```Solidity
mapping(address => uint256) public userNonces;
function updateWorkerDeploymentConfigWithSig(
    address tokenAddress,
    bytes32 projectId,
    bytes32 requestID,
    string memory updatedBase64Config,
    bytes memory signature
) public {
    bytes32 digest = keccak256(abi.encode(
        keccak256("UpdateDeploymentConfig(bytes32 projectId,string updatedBase64Config,string domain,uint256 nonce)"),
        projectId,
        keccak256(bytes(updatedBase64Config)),
        keccak256(bytes("app.crestal.network")),
        userNonces[msg.sender]
    ));
    address signerAddr = getSignerAddress(digest, signature);
    updateWorkerDeploymentConfigCommon(tokenAddress, signerAddr, projectId, requestID, updatedBase64Config);
    userNonces[signerAddr]++;
}
```

---

Related findings:

- [Limitations for Users Employing Multi-sig or Account Abstraction Wallets in Setting Delegate Address](https://0xsimao.com/findings/ostium-limitations-employing-abstraction-delegate): Ostium
- [Users cannot unstake from YiedlETHStakingEtherfi.sol, because YieldAccount.sol is incompatible with ether.fi's WithdrawRequestNFT.sol](https://0xsimao.com/findings/benddao-unstake-eth-staking-yield): BendDAO
- [Bad check in `Vesting.sol::_resetVestingPlans` will prevent users from adding additional liquidity in `SymmVesting.sol`](https://0xsimao.com/findings/symmio-staking-and-vesting-vesting-plans-additional-symm): Symmio, Staking and Vesting
- [reduceOnly limit order update does not take into account that a new amm may be selected, which may lead to loss of funds](https://0xsimao.com/findings/nftperp-i-reduce-account-amm-selected): Nftperp Exchange
