Robinhood MainnetChain ID: 4663
Contract detail

AgentTaxV2

AgentTaxV2 is a verified Robinhood Chain smart contract at 0x4d4e8f06…00ec01bb with 0 indexed transactions.

Address type
Smart contract
Transactions
0
History coverage
No indexed activity found
Verification
Verified
Compiler
v0.8.26+commit.8a97fa7a
Verification match
full
Creation transaction
0xa9915d1b…3a9433b8
Created at block
#191625
Language
solidity
EVM version
paris
License
none
Compiler optimization
Enabled · 200 runs
Verified contract

Contract source code

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol";
import "../pool/IRouter.sol";

/**
 * @dev Minimal interface for BondingV5 to check token launch types
 */
interface IBondingV5ForTaxV2 {
    function isProject60days(address token) external view returns (bool);
    function isProjectXLaunch(address token) external view returns (bool);
    function isAcpSkillLaunch(address token) external view returns (bool);
    function isFeeDelegation(address token) external view returns (bool);
}

/**
 * @title AgentTaxV2
 * @notice Simplified tax contract for V3 tokens with on-chain attribution
 * @dev This contract handles tax collection and distribution for tokens launched via BondingV5
 *      - Uses tokenAddress-based attribution (not agentId)
 *      - No dependency on AgentNft or tax-listener
 *      - Cleaner design with only essential functionality
 */
contract AgentTaxV2 is Initializable, AccessControlUpgradeable {
    using SafeERC20 for IERC20;

    struct TaxRecipient {
        address tba;
        address creator;
    }

    struct TaxAmounts {
        uint256 amountCollected;
        uint256 amountSwapped;
    }

    struct TokenPartnerConfig {
        bytes32 partnerId;
        uint16 partnerFeeRate;
    }

    bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE");
    bytes32 public constant REGISTER_ROLE = keccak256("REGISTER_ROLE");
    bytes32 public constant SWAP_ROLE = keccak256("SWAP_ROLE");
    bytes32 public constant EXECUTOR_ROLE = keccak256("EXECUTOR_ROLE");

    uint256 internal constant DENOM = 10000;

    address public assetToken;
    address public taxToken;
    IRouter public router; // the swap router
    address public treasury;
    uint16 public feeRate;
    uint256 public minSwapThreshold;
    uint256 public maxSwapThreshold;

    mapping(address tokenAddress => TaxRecipient) public tokenRecipients;
    mapping(address tokenAddress => TaxAmounts) public tokenTaxAmounts;

    IBondingV5ForTaxV2 public bondingV5;

    mapping(bytes32 partnerId => address recipient) public partnerRecipients;
    mapping(address tokenAddress => TokenPartnerConfig) public tokenPartnerConfigs;
    /// @dev Upper bound of any configured per-token partner fee; used to guard `updateSwapParams`.
    ///      Not decreased when individual tokens lower their rate (conservative).
    uint16 public maxPartnerFeeRate;

    event TokenRegistered(address indexed tokenAddress, address indexed creator, address tba);
    event TaxDeposited(address indexed tokenAddress, uint256 amount);
    event SwapExecuted(address indexed tokenAddress, uint256 taxTokenAmount, uint256 assetTokenAmount);
    event SwapFailed(address indexed tokenAddress, uint256 taxTokenAmount);
    event CreatorUpdated(address indexed tokenAddress, address oldCreator, address newCreator);
    event SwapParamsUpdated(
        address oldRouter,
        address newRouter,
        address oldAsset,
        address newAsset,
        uint16 oldFeeRate,
        uint16 newFeeRate
    );
    event SwapThresholdUpdated(
        uint256 oldMinThreshold,
        uint256 newMinThreshold,
        uint256 oldMaxThreshold,
        uint256 newMaxThreshold
    );
    event TreasuryUpdated(address oldTreasury, address newTreasury);
    event PartnerRecipientUpdated(
        bytes32 indexed partnerId,
        address oldRecipient,
        address newRecipient
    );
    event TokenPartnerConfigUpdated(
        address indexed tokenAddress,
        bytes32 indexed partnerId,
        uint16 partnerFeeRate
    );
    event PartnerFeeDistributed(
        address indexed tokenAddress,
        bytes32 indexed partnerId,
        address indexed recipient,
        uint256 amount
    );
    event FeeSplitUpdated(uint16 oldProtocolFeeRate, uint16 newProtocolFeeRate);

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

    /**
     * @notice Initialize the contract
     * @param defaultAdmin_ The default admin address
     * @param assetToken_ The asset token address (e.g., USDC)
     * @param taxToken_ The tax token address (e.g., VIRTUAL)
     * @param router_ The Uniswap router address
     * @param treasury_ The treasury address for protocol fees
     * @param minSwapThreshold_ Minimum amount to trigger swap
     * @param maxSwapThreshold_ Maximum amount per swap
     * @param feeRate_ Protocol fee rate (out of 10000)
     */
    function initialize(
        address defaultAdmin_,
        address assetToken_,
        address taxToken_,
        address router_,
        address treasury_,
        uint256 minSwapThreshold_,
        uint256 maxSwapThreshold_,
        uint16 feeRate_
    ) external initializer {
        __AccessControl_init();

        require(assetToken_ != taxToken_, "Asset token cannot be same as tax token");
        require(feeRate_ <= DENOM, "Fee rate too high");

        _grantRole(ADMIN_ROLE, defaultAdmin_);
        _grantRole(DEFAULT_ADMIN_ROLE, defaultAdmin_);

        assetToken = assetToken_;
        taxToken = taxToken_;
        router = IRouter(router_);
        treasury = treasury_;
        minSwapThreshold = minSwapThreshold_;
        maxSwapThreshold = maxSwapThreshold_;
        feeRate = feeRate_;

        IERC20(taxToken).forceApprove(router_, type(uint256).max);

        emit SwapParamsUpdated(address(0), router_, address(0), assetToken_, 0, feeRate_);
        emit SwapThresholdUpdated(0, minSwapThreshold_, 0, maxSwapThreshold_);
    }

    // ============ Core Functions ============

    /**
     * @notice Register a token with its tax recipient (creator and TBA)
     * @dev Called by BondingV5 during launch() to associate token with creator
     * @param tokenAddress The address of the agent token
     * @param tba The Token Bound Address for the agent
     * @param creator The creator address that will receive tax rewards
     */
    function registerToken(
        address tokenAddress,
        address tba,
        address creator
    ) external onlyRole(REGISTER_ROLE) {
        require(tokenAddress != address(0), "Invalid token address");
        require(creator != address(0), "Invalid creator");

        tokenRecipients[tokenAddress] = TaxRecipient({
            tba: tba,
            creator: creator
        });

        emit TokenRegistered(tokenAddress, creator, tba);
    }

    /**
     * @notice Deposit tax for a specific token
     * @dev Called by FRouterV3 and AgentTokenV3 during trades
     *      Caller must have approved this contract to spend taxToken
     * @param tokenAddress The address of the agent token
     * @param amount The amount of tax being deposited
     */
    function depositTax(address tokenAddress, uint256 amount) external {
        // allow deposit tax for any token even if not registered yet, we can still update later
        // TaxRecipient memory recipient = tokenRecipients[tokenAddress];
        // require(recipient.creator != address(0), "Token not registered");
        require(amount > 0, "Amount must be greater than 0");

        IERC20(taxToken).safeTransferFrom(msg.sender, address(this), amount);

        TaxAmounts storage amounts = tokenTaxAmounts[tokenAddress];
        amounts.amountCollected += amount;

        emit TaxDeposited(tokenAddress, amount);
    }

    /**
     * @notice Backend-triggered swap for a specific token's accumulated tax
     * @dev Only backend can trigger swaps to ensure proper price verification.
     *      Backend should verify conversion rate externally before calling.
     * @param tokenAddress The address of the agent token to swap taxes for
     * @param minOutput Minimum output amount (calculated by backend with proper price check)
     */
    function swapForTokenAddress(
        address tokenAddress,
        uint256 minOutput
    ) external onlyRole(SWAP_ROLE) {
        TaxRecipient memory recipient = tokenRecipients[tokenAddress];
        require(recipient.creator != address(0), "Token not registered");

        TaxAmounts storage amounts = tokenTaxAmounts[tokenAddress];
        _swapAndDistribute(tokenAddress, recipient, amounts, minOutput);
    }

    /**
     * @notice Batch swap for multiple tokens
     * @param tokenAddresses Array of token addresses to swap
     * @param minOutputs Array of minimum output amounts (same length as tokenAddresses)
     */
    function batchSwapForTokenAddress(
        address[] memory tokenAddresses,
        uint256[] memory minOutputs
    ) external onlyRole(SWAP_ROLE) {
        require(tokenAddresses.length == minOutputs.length, "Length mismatch");

        // Trade-off: keep per-token swap/distribution instead of aggregating all pending tax
        // into one large swap. This path is currently low-frequency, so we prioritize
        // correctness/isolation over gas optimization:
        // - Per-token minOutput/slippage checks remain explicit.
        // - Per-token accounting and recipient attribution stay simple/auditable.
        // - A single token issue does not block others in the batch.
        // If execution frequency increases materially, revisit aggregate-swap design.
        for (uint i = 0; i < tokenAddresses.length; i++) {
            address tokenAddress = tokenAddresses[i];
            TaxRecipient memory recipient = tokenRecipients[tokenAddress];
            
            if (recipient.creator == address(0)) {
                continue;
            }

            TaxAmounts storage amounts = tokenTaxAmounts[tokenAddress];
            _swapAndDistribute(tokenAddress, recipient, amounts, minOutputs[i]);
        }
    }

/**
     * @notice Update tax recipient for special launch agents (Project60days, X_LAUNCH, or ACP_SKILL)
     * @dev Called by backend for tokens that need creator updates:
     * 
     *      Project60days tokens:
     *        - After graduation: set creator = vaultAddress (60-day lock)
     *        - After COMMIT: set creator = walletAddress (creator receives tax)
     * 
     *      X_LAUNCH / ACP_SKILL tokens:
     *        - Before launch: set creator = taxRecipient (partner address)
     * 
     * @param tokenAddress The address of the agent token
     * @param tba The Token Bound Address (usually same as dbVirtuals.walletAddress)
     * @param creator The creator address that will receive tax rewards
     */
    function updateCreatorForSpecialLaunchAgents(
        address tokenAddress,
        address tba,
        address creator
    ) external onlyRole(EXECUTOR_ROLE) {
        require(address(bondingV5) != address(0), "BondingV5 not set");
        require(tokenAddress != address(0), "Invalid token address");
        require(tba != address(0), "Invalid TBA");
        require(creator != address(0), "Invalid creator");

        bool isSpecialLaunch = bondingV5.isProject60days(tokenAddress) ||
            bondingV5.isProjectXLaunch(tokenAddress) ||
            bondingV5.isAcpSkillLaunch(tokenAddress) ||
            bondingV5.isFeeDelegation(tokenAddress);

        require(isSpecialLaunch, "Token is not a special launch type");

        TaxRecipient storage recipient = tokenRecipients[tokenAddress];
        require(recipient.creator != address(0), "Token not registered");

        address oldCreator = recipient.creator;
        recipient.tba = tba;
        recipient.creator = creator;

        emit CreatorUpdated(tokenAddress, oldCreator, creator);
    }

    // ============ Internal Functions ============

    /**
     * @notice Internal function to swap accumulated tax and distribute
     */
    function _swapAndDistribute(
        address tokenAddress,
        TaxRecipient memory recipient,
        TaxAmounts storage amounts,
        uint256 minOutput
    ) internal {
        uint256 amountToSwap = amounts.amountCollected - amounts.amountSwapped;

        if (amountToSwap < minSwapThreshold) {
            return;
        }

        if (amountToSwap > maxSwapThreshold) {
            amountToSwap = maxSwapThreshold;
        }

        uint256 balance = IERC20(taxToken).balanceOf(address(this));
        if (balance < amountToSwap) {
            return;
        }

        address[] memory path = new address[](2);
        path[0] = taxToken;
        path[1] = assetToken;

        try
            router.swapExactTokensForTokens(
                amountToSwap,
                minOutput,
                path,
                address(this),
                block.timestamp + 300
            )
        returns (uint256[] memory swapAmounts) {
            uint256 assetReceived = swapAmounts[1];
            emit SwapExecuted(tokenAddress, amountToSwap, assetReceived);

            uint256 protocolFee = (assetReceived * feeRate) / DENOM;

            TokenPartnerConfig memory partnerConfig = tokenPartnerConfigs[tokenAddress];
            uint256 partnerFee = (assetReceived * partnerConfig.partnerFeeRate) / DENOM;

            // Cap fees if protocol + partner rates exceed 100% (e.g. after feeRate was raised
            // without re-validating partner configs). Prevents underflow from bricking swaps.
            uint256 remaining = assetReceived;
            if (protocolFee > remaining) {
                protocolFee = remaining;
            }
            remaining -= protocolFee;
            if (partnerFee > remaining) {
                partnerFee = remaining;
            }
            remaining -= partnerFee;
            uint256 creatorFee = remaining;

            if (partnerFee > 0) {
                address partnerRecipient = partnerRecipients[partnerConfig.partnerId];
                require(
                    partnerRecipient != address(0),
                    "Partner recipient not set"
                );
                IERC20(assetToken).safeTransfer(partnerRecipient, partnerFee);
                emit PartnerFeeDistributed(
                    tokenAddress,
                    partnerConfig.partnerId,
                    partnerRecipient,
                    partnerFee
                );
            }

            if (creatorFee > 0) {
                IERC20(assetToken).safeTransfer(recipient.creator, creatorFee);
            }

            if (protocolFee > 0) {
                IERC20(assetToken).safeTransfer(treasury, protocolFee);
            }

            amounts.amountSwapped += amountToSwap;
        } catch {
            emit SwapFailed(tokenAddress, amountToSwap);
        }
    }

    // ============ View Functions ============

    /**
     * @notice Get the tax recipient for a token
     */
    function getTokenRecipient(
        address tokenAddress
    ) external view returns (address tba, address creator) {
        TaxRecipient memory recipient = tokenRecipients[tokenAddress];
        return (recipient.tba, recipient.creator);
    }

    /**
     * @notice Get tax amounts for a token
     */
    function getTokenTaxAmounts(
        address tokenAddress
    ) external view returns (uint256 amountCollected, uint256 amountSwapped) {
        TaxAmounts memory amounts = tokenTaxAmounts[tokenAddress];
        return (amounts.amountCollected, amounts.amountSwapped);
    }

    /**
     * @notice Get partner fee configuration for an agent token
     */
    function getTokenPartnerConfig(
        address tokenAddress
    ) external view returns (bytes32 partnerId, uint16 partnerFeeRate) {
        TokenPartnerConfig memory config = tokenPartnerConfigs[tokenAddress];
        return (config.partnerId, config.partnerFeeRate);
    }

    // ============ Creator Functions ============

    /**
     * @notice Update the creator for a registered token
     * @dev Only current creator or admin can update
     */
    function updateCreator(
        address tokenAddress,
        address newCreator
    ) external {
        TaxRecipient storage recipient = tokenRecipients[tokenAddress];
        require(recipient.creator != address(0), "Token not registered");
        require(newCreator != address(0), "Invalid new creator");

        address sender = _msgSender();
        require(
            sender == recipient.creator || hasRole(ADMIN_ROLE, sender),
            "Only creator or admin can update"
        );

        address oldCreator = recipient.creator;
        recipient.creator = newCreator;

        emit CreatorUpdated(tokenAddress, oldCreator, newCreator);
    }

    // ============ Admin Functions ============

    /**
     * @notice Set the receiving address for a partner / referrer integration
     * @param partnerId Unique partner identifier
     * @param recipient Address that receives partner fee shares
     */
    function setPartnerRecipient(
        bytes32 partnerId,
        address recipient
    ) external onlyRole(ADMIN_ROLE) {
        require(partnerId != bytes32(0), "Invalid partner ID");
        require(recipient != address(0), "Invalid recipient");

        address oldRecipient = partnerRecipients[partnerId];
        partnerRecipients[partnerId] = recipient;

        emit PartnerRecipientUpdated(partnerId, oldRecipient, recipient);
    }

    /**
     * @notice Configure partner fee split for an agent token
     * @dev Partner fee is deducted from the total swapped asset amount alongside the
     *      protocol fee. Remaining amount goes to the token creator.
     *      `feeRate + partnerFeeRate` must not exceed DENOM (100%).
     *      First configuration per token: EXECUTOR_ROLE or ADMIN_ROLE.
     *      Subsequent updates: ADMIN_ROLE only.
     * @param tokenAddress The agent token address
     * @param partnerId Partner identifier (must have recipient configured if rate > 0)
     * @param partnerFeeRate Partner share in basis points (out of 10000)
     */
    function setTokenPartnerConfig(
        address tokenAddress,
        bytes32 partnerId,
        uint16 partnerFeeRate
    ) external {
        address sender = _msgSender();
        TokenPartnerConfig memory existing = tokenPartnerConfigs[tokenAddress];
        bool isUnset =
            existing.partnerId == bytes32(0) && existing.partnerFeeRate == 0;

        if (isUnset) {
            require(
                hasRole(EXECUTOR_ROLE, sender) || hasRole(ADMIN_ROLE, sender),
                "Only executor or admin can set partner config"
            );
        } else {
            require(hasRole(ADMIN_ROLE, sender), "Only admin can update partner config");
        }

        require(tokenAddress != address(0), "Invalid token address");
        require(feeRate + partnerFeeRate <= DENOM, "Fee split exceeds 100%");

        if (partnerFeeRate > 0) {
            require(partnerId != bytes32(0), "Partner ID required");
            require(
                partnerRecipients[partnerId] != address(0),
                "Partner recipient not set"
            );
        }

        tokenPartnerConfigs[tokenAddress] = TokenPartnerConfig({
            partnerId: partnerId,
            partnerFeeRate: partnerFeeRate
        });

        if (partnerFeeRate > maxPartnerFeeRate) {
            maxPartnerFeeRate = partnerFeeRate;
        }

        emit TokenPartnerConfigUpdated(tokenAddress, partnerId, partnerFeeRate);
    }

    /**
     * @notice Set BondingV5 contract address
     * @param bondingV5_ The address of the BondingV5 contract
     */
    function setBondingV5(address bondingV5_) external onlyRole(ADMIN_ROLE) {
        require(bondingV5_ != address(0), "Invalid BondingV5 address");
        bondingV5 = IBondingV5ForTaxV2(bondingV5_);
    }

    /**
     * @notice Update swap parameters
     */
    function updateSwapParams(
        address router_,
        address assetToken_,
        uint16 feeRate_
    ) external onlyRole(ADMIN_ROLE) {
        require(feeRate_ <= DENOM, "Fee rate too high");
        require(
            feeRate_ + maxPartnerFeeRate <= DENOM,
            "Fee split exceeds 100%"
        );

        address oldRouter = address(router);
        address oldAsset = assetToken;
        uint16 oldFee = feeRate;

        IERC20(taxToken).forceApprove(oldRouter, 0);
        IERC20(taxToken).forceApprove(router_, type(uint256).max);

        router = IRouter(router_);
        assetToken = assetToken_;
        feeRate = feeRate_;

        emit SwapParamsUpdated(oldRouter, router_, oldAsset, assetToken_, oldFee, feeRate_);
    }

    /**
     * @notice Update swap thresholds
     */
    function updateSwapThresholds(
        uint256 minSwapThreshold_,
        uint256 maxSwapThreshold_
    ) external onlyRole(ADMIN_ROLE) {
        uint256 oldMin = minSwapThreshold;
        uint256 oldMax = maxSwapThreshold;

        minSwapThreshold = minSwapThreshold_;
        maxSwapThreshold = maxSwapThreshold_;

        emit SwapThresholdUpdated(oldMin, minSwapThreshold_, oldMax, maxSwapThreshold_);
    }

    /**
     * @notice Update treasury address
     */
    function updateTreasury(address treasury_) external onlyRole(ADMIN_ROLE) {
        require(treasury_ != address(0), "Invalid treasury");
        address oldTreasury = treasury;
        treasury = treasury_;
        emit TreasuryUpdated(oldTreasury, treasury_);
    }

    /**
     * @notice Emergency withdraw tokens
     */
    function withdraw(address token) external onlyRole(ADMIN_ROLE) {
        uint256 balance = IERC20(token).balanceOf(address(this));
        if (balance > 0) {
            IERC20(token).safeTransfer(treasury, balance);
        }
    }
}