HIGH severity: The tax in agent tokens is causing incorrect accounting in the AgentImageService. When users transfer agent tokens with a tax mechanism (e.g., via `transfer` in AgentToken.sol), a portion of the tokens is deducted as tax before reaching the recipient. However, the `requestImage` function in AgentImageService assumes the full `finalImagePrice` is received, without accounting for p...

Vulnerable code:
// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;

import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol";
import "@openzeppelin/contracts-upgradeable/utils/AddressUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import "./interfaces/IAgentBalances.sol";
import "./interfaces/IAutonomousAgentDeployer.sol";

contract AgentToken is Initializable, ERC20Upgradeable, UUPSUpgradeable {
    using AddressUpgradeable for address;

    uint256 public taxPercentage;
    address public taxWallet;
    address public owner;
    IAgentBalances public agentBalances;

    uint8 version;
    // Some agents are already deployed and need upgrading using diamond pattern
    // IAutonomousAgentDeployer constant public deployer = IAutonomousAgentDeployer(address(0xD84B6CAccFCc9FA5F48c6277C40FaC0620f1d0c2));
    // use this if testing or deploying new deployer
    IAutonomousAgentDeployer public deployer;

    uint256[49] private __gap;

    event TaxChanged(uint256 taxPercentage);
    event TaxWalletchanged(address taxWallet);

    modifier onlyUpgrader() {
        require(owner == msg.sender, "CALLER_NOT_UPGRADER");
        _;
    }

    function initialize(
        string memory name,
        string memory symbol,
        uint256 initialSupply,
        address _owner,
        address _agentBalances
    ) public initializer {
        require(_owner != address(0), "Invalid owner");
        __ERC20_init(name, symbol);
        __UUPSUpgradeable_init(); //initialize the UUPSUpgradeable
        owner = _owner;
        taxWallet = _owner; // default tax wallet
        taxPercentage = 100;
        agentBalances = IAgentBalances(_agentBalances);
        deployer = IAutonomousAgentDeployer(msg.sender);
        _mint(msg.sender, initialSupply);
        version = 1;
    }


    // Override the transfer function to apply tax only if the recipient is a contract
    function transfer(address recipient, uint256 amount) public override returns (bool) {
        // Check if the recipient is agentBalances to prevent recursion
        if (recipient == address(agentBalances)) {
            return super.transfer(recipient, amount);
        }
        address distributor = address(deployer.distributor());
        if (address(deployer) != address(0) &&                               // deployer exists
        (recipient.isContract() || msg.sender.isContract()) &&                   // XOR: exactly one must be a contract
        !(recipient == address(deployer) || msg.sender == address(deployer)) &&  // neither sender nor recipient is deployer
        (distributor == address(0) || recipient != distributor))
        {
            uint256 taxAmount = (amount * taxPercentage) / 10000;
            uint256 amountAfterTax = amount - taxAmount;

            //send the tax to deployer to distribute
            super.transfer(address(deployer), taxAmount);
            deployer.accumulateSwapFees(taxAmount);

            // Transfer the remaining amount to the recipient
            return super.transfer(recipient, amountAfterTax);
        } else {
            // No tax if recipient is not a contract
            return super.transfer(recipient, amount);
        }
    }

    // Override the transferFrom function to apply tax only if the recipient is a contract
    function transferFrom(address sender, address recipient, uint256 amount) public override returns (bool) {
        // Check if the recipient is agentBalances to prevent recursion
        if (recipient == address(agentBalances)) {
            return super.transferFrom(sender, recipient, amount);
        }

        address distributor = address(deployer.distributor());

        if (address(deployer) != address(0) &&                               // deployer exists
        (recipient.isContract() || sender.isContract()) &&                   // XOR: exactly one must be a contract
        !(recipient == address(deployer) || sender == address(deployer)) &&  // neither sender nor recipient is deployer
        (distributor == address(0) || recipient != distributor))
        {
            uint256 taxAmount = (amount * taxPercentage) / 10000;
            uint256 amountAfterTax = amount - taxAmount;

            //send the tax to deployer to distribute
            super.transferFrom(sender, address(deployer), taxAmount);
            deployer.accumulateSwapFees(taxAmount);

            // Transfer the remaining amount to the recipient
            return super.transferFrom(sender, recipient, amountAfterTax);
        } else {
            // No tax if recipient is not a contract
            return super.transferFrom(sender, recipient, amount);
        }
    }

    function setTaxWallet(address _taxWallet) external {
        require(msg.sender == owner, "Only the owner can set the tax wallet");
        require(_taxWallet != address(0), "Invalid tax wallet address");
        taxWallet = _taxWallet;

        emit TaxWalletchanged(_taxWallet);
    }

    // Times the percentage by 100 to allow for more decimal places
    function setTaxPercentage(uint256 _taxPercentage) external {
        require(_taxPercentage >= 100, "Tax percentage too small");
        require(_taxPercentage <= 10000, "Tax percentage cannot exceed 100%");
        require(msg.sender == owner, "Only the owner can set the tax percentage");
        taxPercentage = _taxPercentage;
        emit TaxChanged(_taxPercentage);
    }

    function _authorizeUpgrade(address newImplementation) internal override onlyUpgrader {
        require(newImplementation != address(0), "ADDRESS_IS_ZERO");
        ++version;
    }

    function removeAllowance(address approver, address spender) public onlyUpgrader {
        _approve(approver, spender, 0);
    }
}
// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;

import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "./interfaces/IAutonomousAgentDeployer.sol";
import "./interfaces/IOctoDistributor.sol";

contract AgentImageService is Initializable, OwnableUpgradeable, ReentrancyGuardUpgradeable, UUPSUpgradeable {
    using SafeERC20Upgradeable for IERC20Upgradeable;

    struct ImageRequest {
        address user;
        uint256 amount;
        uint256 width;
        uint256 height;
        uint256 timestamp;
        uint256 fulfilled;
        uint256 refunded;
        string prompt;
    }
    struct Agent {
        string agentName;
        string agentTicker;
        string agentDescription;
    }

    struct AgentConfig {
        uint256 pricePerImage;
        string imageDescription;
        uint256 refundTimeLimit; // Time in seconds after which refund is possible
        bool isConfigured;
    }

    enum Parameter {
        TREASURY_CUT
    }

    mapping(address => Agent) public agents;
    // Agent token address => Agent configuration
    mapping(address => AgentConfig) public agentConfigs;
    
    // Agent token address => accumulated fees ready for withdrawal
    mapping(address => uint256) public accumulatedFees;
    
    // Agent token address => amount locked in pending requests
    mapping(address => uint256) public pendingFees;
    
    // Agent token address => request hash => ImageRequest
    mapping(address => mapping(bytes32 => ImageRequest)) public imageRequests;
    
    // Agent token address => all request IDs for that agent
    mapping(address => bytes32[]) private agentRequestIds;
    
    // Agent token address => index => whether request has been processed (fulfilled or refunded)
    mapping(address => mapping(bytes32 => bool)) private requestProcessed;

    mapping(Parameter => uint256) public parameters;
    
    IAutonomousAgentDeployer public deployer;
    address public spectralTreasury;

    uint16 public version;

    uint256[50] __gap; // Reserved space

    event AgentConfigured(address indexed agentToken, uint256 price, string description, uint256 refundTimeLimit);
    event ImageRequested(address indexed agentToken, address indexed user, bytes32 indexed requestId, uint256 amount, uint256 width, uint256 height, string prompt);
    event ImageFulfilled(address indexed agentToken, bytes32 indexed requestId);
    event FeesWithdrawn(address indexed agentToken, uint256 amount);
    event RefundIssued(address indexed agentToken, bytes32 indexed requestId, uint256 amount);
    event ImageAgentDeployed(string agentName, string agentTicker, string agentDescription, address indexed owner, address indexed agentToken, uint256 minSpecAmount, uint256 tokenAmountOut);

    /// @custom:oz-upgrades-unsafe-allow constructor
    constructor() {
        _disableInitializers();
    }

    function initialize(address _deployer, address _spectralTreasury, uint16 _treasuryCut) public initializer {
        require(_deployer != address(0), "AgentImageService: deployer cannot be zero address");
        require(_spectralTreasury != address(0), "AgentImageService: spectral treasury cannot be zero address");
        __Ownable_init();
        __ReentrancyGuard_init();
        __UUPSUpgradeable_init();
        parameters[Parameter.TREASURY_CUT] = _treasuryCut;
        spectralTreasury = _spectralTreasury;
        deployer = IAutonomousAgentDeployer(_deployer);
        version = 1;
    }

    modifier onlyAgentOwner(address agentToken) {
        require(IOctoDistributor(address(deployer.distributor())).agentCreators(agentToken) == msg.sender, "AgentImageService: : Not agent owner");
        _;
    }

    modifier onlyValidAgent(address agentToken) {
        require(deployer.isAgentToken(agentToken), "AgentImageService: : Invalid agent token");
        _;
    }

    function getAgentCreator(address agentToken) external view returns (address) {
        return IOctoDistributor(address(deployer.distributor())).agentCreators(agentToken);
    }

    function deployImageAgent(
        string memory agentName,
        string memory agentTicker,
        string memory agentDescription,
        address owner,
        uint256 minSpecAmount) external payable returns (address agentToken) {
        require(owner != address(0), "AgentImageService: Owner cannot be zero address");
        require(msg.value >= deployer.parameters(IAutonomousAgentDeployer.Parameter.DEPLOYMENT_COST_ETH), "AgentImageService: Incorrect ETH amount sent for deployment");
        // only transfer ETH to the this contract from the user in the next lines
        (agentToken) = deployer.deployAgentWithETH{value: msg.value}(agentName, agentTicker, minSpecAmount);
        agents[agentToken] = Agent({
            agentName: agentName,
            agentTicker: agentTicker,
            agentDescription: agentDescription
        });
        agentConfigs[agentToken] = AgentConfig({
            pricePerImage: 1 ether, // Default to 0 until configured
            imageDescription: "",
            refundTimeLimit: 1 days, // Default to 0 until configured
            isConfigured: true
        });
        uint256 tokenAmountOut = IERC20Upgradeable(agentToken).balanceOf(address(this));
        IERC20Upgradeable(agentToken).safeTransfer(owner, tokenAmountOut);
        emit ImageAgentDeployed(agentName, agentTicker, agentDescription, owner, agentToken, minSpecAmount, tokenAmountOut);

    }

    function _configureAgent(
        address agentToken,
        uint256 pricePerImage,
        string memory imageDescription,
        uint256 refundTimeLimit
    ) internal {
        require(pricePerImage > 0, "Price must be greater than 0");
        require(refundTimeLimit > 0, "Refund time limit must be greater than 0");
        
        agentConfigs[agentToken] = AgentConfig({
            pricePerImage: pricePerImage,
            imageDescription: imageDescription,
            refundTimeLimit: refundTimeLimit,
            isConfigured: true
        });
    }

    function configureAgent(
        address agentToken,
        uint256 pricePerImage,
        string memory imageDescription,
        uint256 refundTimeLimit
    ) external onlyAgentOwner(agentToken) onlyValidAgent(agentToken) {
        require(!agentConfigs[agentToken].isConfigured, "Agent already configured");
        _configureAgent(agentToken, pricePerImage, imageDescription, refundTimeLimit);

        emit AgentConfigured(agentToken, pricePerImage, imageDescription, refundTimeLimit);
    }

    function requestImage(
        address agentToken,
        string calldata prompt,
        uint256 width,
        uint256 height
    ) external nonReentrant onlyValidAgent(agentToken) {
        AgentConfig memory config = agentConfigs[agentToken];
        require(config.isConfigured, "Agent not configured");
        require(bytes(prompt).length > 0, "Prompt cannot be empty");
        require(width >= 64 && height >= 64, "Width and height must be greater or equal than 64");
        IERC20Upgradeable token = IERC20Upgradeable(agentToken);
        
        // Generate request ID from prompt and timestamp
        bytes32 requestId = keccak256(abi.encodePacked(prompt, block.timestamp, msg.sender));
        
        // Transfer tokens from user to this contract
        token.safeTransferFrom(msg.sender, address(this), config.pricePerImage);
        
        // Record the request
        imageRequests[agentToken][requestId] = ImageRequest({
            user: msg.sender,
            amount: config.pricePerImage,
            width: width,
            height: height,
            timestamp: block.timestamp,
            fulfilled: 0,
            refunded: 0,
            prompt: prompt
        });

        // Add to pending fees instead of accumulated
        pendingFees[agentToken] += config.pricePerImage;
        
        emit ImageRequested(agentToken, msg.sender, requestId, config.pricePerImage, width, height, prompt);
    }

    function fulfillImage(
        address agentToken,
        bytes32 requestId
    ) external onlyAgentOwner(agentToken) onlyValidAgent(agentToken) nonReentrant {
        ImageRequest storage request = imageRequests[agentToken][requestId];
        require(request.fulfilled == 0 && request.refunded == 0, "Request already processed");
        require(request.user != address(0), "Request does not exist");
        
        request.fulfilled = block.timestamp;
        
        // Move fee from pending to accumulated
        pendingFees[agentToken] -= request.amount;
        uint256 treasuryCut = (request.amount * parameters[Parameter.TREASURY_CUT]) / 10000;
        accumulatedFees[agentToken] += (request.amount - treasuryCut);
        IERC20Upgradeable(agentToken).safeTransfer(spectralTreasury, (request.amount * parameters[Parameter.TREASURY_CUT]) / 10000);
        
        emit ImageFulfilled(agentToken, requestId);
    }

    function requestRefund(
        address agentToken,
        bytes32 requestId
    ) external nonReentrant {
        ImageRequest storage request = imageRequests[agentToken][requestId];
        AgentConfig memory config = agentConfigs[agentToken];
        
        require(request.user == msg.sender, "Not request owner");
        require(request.fulfilled == 0 && request.refunded == 0, "Request already processed");
        require(
            block.timestamp >= request.timestamp + config.refundTimeLimit,
            "Refund time limit not reached"
        );

        request.refunded = block.timestamp;
        
        // Remove from pending fees
        pendingFees[agentToken] -= request.amount;
        
        // Transfer tokens back to user
        IERC20Upgradeable(agentToken).safeTransfer(msg.sender, request.amount);
        
        emit RefundIssued(agentToken, requestId, request.amount);
    }

    function withdrawFees(address agentToken) external onlyAgentOwner(agentToken) nonReentrant {
        uint256 amount = accumulatedFees[agentToken];
        require(amount > 0, "No fees to withdraw");
        
        accumulatedFees[agentToken] = 0;
        IERC20Upgradeable(agentToken).safeTransfer(msg.sender, amount);
        
        emit FeesWithdrawn(agentToken, amount);
    }

    function getRequestDetails(
        address agentToken,
        bytes32 requestId
    ) external view returns (
        address user,
        uint256 amount,
        uint256 timestamp,
        uint256 fulfilled,
        uint256 refunded,
        string memory prompt
    ) {
        ImageRequest memory request = imageRequests[agentToken][requestId];
        return (
            request.user,
            request.amount,
            request.timestamp,
            request.fulfilled,
            request.refunded,
            request.prompt
        );
    }

    // Helper function to check if an agent has any pending fees
    function getPendingFees(address agentToken) public view returns (uint256) {
        return pendingFees[agentToken];
    }

    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {
        require(newImplementation != address(0), "ZERO_ADDRESS");
        ++version;
    }
} 

Vulnerability type: CWE-707|CWE-20