Policy client
TotpTransferPolicyClient
Solidity for the Newton PolicyClient Safe module that executes an attested USDC transfer after Newton allows it.
0xbE59C5d05FE46AA532c2e5c2c2E3958fAe01AE6f
Attack demo · Policy · Guard
policy-client/src/TotpTransferPolicyClient.sol
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.27;
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {INewtonProverTaskManager} from "newton-contracts/src/interfaces/INewtonProverTaskManager.sol";
import {NewtonMessage} from "newton-contracts/src/core/NewtonMessage.sol";
import {NewtonPolicyClient} from "newton-contracts/src/mixins/NewtonPolicyClient.sol";
import {ISafe} from "./ISafe.sol";
/// @title TotpTransferPolicyClient
/// @notice Narrow Safe module: attested USDC transfer from the honey-pot Safe.
/// @dev Intent describes `transfer(address recipient, uint256 amount, bytes32 tapNonce)`
/// with `intent.to` = the token and `intent.from` = the compromised spender EOA
/// (`msg.sender`). Execution is Safe `execTransactionFromModule` → `USDC.transfer`.
/// The compromised key is a Safe owner; OwnerExecGuard blocks owner exec.
/// Task replay is TaskManager `taskId` spend via `_validateAttestationDirect`.
/// Tap reuse is this contract's `usedTap` map, keyed by the attested tapNonce.
contract TotpTransferPolicyClient is NewtonPolicyClient {
error InvalidIntentTarget(address expected, address actual);
error InvalidIntentValue(uint256 value);
error InvalidFunctionSignature();
error InvalidCalldata();
error InvalidSelector(bytes4 expected, bytes4 actual);
error IntentArgumentsMismatch();
error TapNonceAlreadyUsed(bytes32 tapNonce);
error PolicyDenied();
error ModuleExecFailed();
bytes internal constant TRANSFER_SIGNATURE =
bytes("function transfer(address recipient, uint256 amount, bytes32 tapNonce)");
bytes4 internal constant TRANSFER_SELECTOR = bytes4(keccak256("transfer(address,uint256,bytes32)"));
IERC20 public immutable token;
ISafe public immutable safe;
mapping(bytes32 => bool) public usedTap;
event ProtectedTransfer(address indexed sender, address indexed recipient, uint256 amount, bytes32 tapNonce);
constructor(
address token_,
address safe_,
address policyTaskManager_,
address policy_,
address policyClientOwner_
) {
require(token_ != address(0) && safe_ != address(0) && policyTaskManager_ != address(0) && policy_ != address(0));
require(policyClientOwner_ != address(0));
token = IERC20(token_);
safe = ISafe(safe_);
_initNewtonPolicyClient(policyTaskManager_, policyClientOwner_);
_setPolicyAddress(policy_);
}
/// @notice Transfer USDC from the Safe after a directly verified Newton policy response.
function transferWithAttestation(
address recipient,
uint256 amount,
bytes32 tapNonce,
INewtonProverTaskManager.Task calldata task,
INewtonProverTaskManager.TaskResponse calldata taskResponse,
bytes calldata signatureData
) external {
NewtonMessage.Intent calldata intent = taskResponse.intent;
require(intent.to == address(token), InvalidIntentTarget(address(token), intent.to));
require(intent.value == 0, InvalidIntentValue(intent.value));
require(keccak256(intent.functionSignature) == keccak256(TRANSFER_SIGNATURE), InvalidFunctionSignature());
require(intent.data.length == 100, InvalidCalldata());
bytes4 selector = bytes4(intent.data[:4]);
require(selector == TRANSFER_SELECTOR, InvalidSelector(TRANSFER_SELECTOR, selector));
(address attestedRecipient, uint256 attestedAmount, bytes32 attestedTapNonce) =
abi.decode(intent.data[4:], (address, uint256, bytes32));
require(
attestedRecipient == recipient && attestedAmount == amount && attestedTapNonce == tapNonce,
IntentArgumentsMismatch()
);
require(_validateAttestationDirect(task, taskResponse, signatureData), PolicyDenied());
require(!usedTap[attestedTapNonce], TapNonceAlreadyUsed(attestedTapNonce));
usedTap[attestedTapNonce] = true;
bytes memory callData = abi.encodeWithSelector(IERC20.transfer.selector, recipient, amount);
bool ok = safe.execTransactionFromModule(address(token), 0, callData, ISafe.Operation.Call);
require(ok, ModuleExecFailed());
emit ProtectedTransfer(msg.sender, recipient, amount, attestedTapNonce);
}
}