From 57c9b167a0954900241efdb21e927f689a452fb4 Mon Sep 17 00:00:00 2001 From: Weixiong Tay Date: Fri, 29 May 2026 13:48:28 +0800 Subject: [PATCH 1/3] feat: v2 partnerid changes --- contracts/tax/AgentTaxV2.sol | 118 ++++++++++++++++++++++++++++++++++- 1 file changed, 117 insertions(+), 1 deletion(-) diff --git a/contracts/tax/AgentTaxV2.sol b/contracts/tax/AgentTaxV2.sol index 3bd0e20..402f982 100644 --- a/contracts/tax/AgentTaxV2.sol +++ b/contracts/tax/AgentTaxV2.sol @@ -38,6 +38,11 @@ contract AgentTaxV2 is Initializable, AccessControlUpgradeable { 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"); @@ -58,6 +63,9 @@ contract AgentTaxV2 is Initializable, AccessControlUpgradeable { IBondingV5ForTaxV2 public bondingV5; + mapping(bytes32 partnerId => address recipient) public partnerRecipients; + mapping(address tokenAddress => TokenPartnerConfig) public tokenPartnerConfigs; + 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); @@ -78,6 +86,23 @@ contract AgentTaxV2 is Initializable, AccessControlUpgradeable { 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() { @@ -307,7 +332,21 @@ contract AgentTaxV2 is Initializable, AccessControlUpgradeable { emit SwapExecuted(tokenAddress, amountToSwap, assetReceived); uint256 protocolFee = (assetReceived * feeRate) / DENOM; - uint256 creatorFee = assetReceived - protocolFee; + + TokenPartnerConfig memory partnerConfig = tokenPartnerConfigs[tokenAddress]; + uint256 partnerFee = (assetReceived * partnerConfig.partnerFeeRate) / DENOM; + uint256 creatorFee = assetReceived - protocolFee - partnerFee; + + if (partnerFee > 0) { + address partnerRecipient = partnerRecipients[partnerConfig.partnerId]; + IERC20(assetToken).safeTransfer(partnerRecipient, partnerFee); + emit PartnerFeeDistributed( + tokenAddress, + partnerConfig.partnerId, + partnerRecipient, + partnerFee + ); + } if (creatorFee > 0) { IERC20(assetToken).safeTransfer(recipient.creator, creatorFee); @@ -345,6 +384,16 @@ contract AgentTaxV2 is Initializable, AccessControlUpgradeable { 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 ============ /** @@ -373,6 +422,73 @@ contract AgentTaxV2 is Initializable, AccessControlUpgradeable { // ============ 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%). + * @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 onlyRole(ADMIN_ROLE) { + 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 + }); + + emit TokenPartnerConfigUpdated(tokenAddress, partnerId, partnerFeeRate); + } + + /** + * @notice Update the global protocol (treasury) fee rate + * @dev Per-token partner fees are configured via setTokenPartnerConfig(). + * After updating, ensure `feeRate + partnerFeeRate <= DENOM` for all tokens + * with an active partner split. + * @param protocolFeeRate_ Protocol share in basis points (out of 10000) + */ + function updateFeeSplit(uint16 protocolFeeRate_) external onlyRole(ADMIN_ROLE) { + require(protocolFeeRate_ <= DENOM, "Fee rate too high"); + + uint16 oldFeeRate = feeRate; + feeRate = protocolFeeRate_; + + emit FeeSplitUpdated(oldFeeRate, protocolFeeRate_); + } + /** * @notice Set BondingV5 contract address * @param bondingV5_ The address of the BondingV5 contract From a8fd11993e284197ccb9894c621fd2c07b685640 Mon Sep 17 00:00:00 2001 From: Weixiong Tay Date: Fri, 29 May 2026 16:13:46 +0800 Subject: [PATCH 2/3] fix: updated: --- contracts/tax/AgentTaxV2.sol | 59 +++++++++++++++++++++++++----------- 1 file changed, 41 insertions(+), 18 deletions(-) diff --git a/contracts/tax/AgentTaxV2.sol b/contracts/tax/AgentTaxV2.sol index 402f982..897e617 100644 --- a/contracts/tax/AgentTaxV2.sol +++ b/contracts/tax/AgentTaxV2.sol @@ -65,6 +65,9 @@ contract AgentTaxV2 is Initializable, AccessControlUpgradeable { 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); @@ -335,7 +338,19 @@ contract AgentTaxV2 is Initializable, AccessControlUpgradeable { TokenPartnerConfig memory partnerConfig = tokenPartnerConfigs[tokenAddress]; uint256 partnerFee = (assetReceived * partnerConfig.partnerFeeRate) / DENOM; - uint256 creatorFee = assetReceived - protocolFee - partnerFee; + + // 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]; @@ -445,6 +460,8 @@ contract AgentTaxV2 is Initializable, AccessControlUpgradeable { * @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) @@ -453,7 +470,21 @@ contract AgentTaxV2 is Initializable, AccessControlUpgradeable { address tokenAddress, bytes32 partnerId, uint16 partnerFeeRate - ) external onlyRole(ADMIN_ROLE) { + ) 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%"); @@ -470,23 +501,11 @@ contract AgentTaxV2 is Initializable, AccessControlUpgradeable { partnerFeeRate: partnerFeeRate }); - emit TokenPartnerConfigUpdated(tokenAddress, partnerId, partnerFeeRate); - } - - /** - * @notice Update the global protocol (treasury) fee rate - * @dev Per-token partner fees are configured via setTokenPartnerConfig(). - * After updating, ensure `feeRate + partnerFeeRate <= DENOM` for all tokens - * with an active partner split. - * @param protocolFeeRate_ Protocol share in basis points (out of 10000) - */ - function updateFeeSplit(uint16 protocolFeeRate_) external onlyRole(ADMIN_ROLE) { - require(protocolFeeRate_ <= DENOM, "Fee rate too high"); - - uint16 oldFeeRate = feeRate; - feeRate = protocolFeeRate_; + if (partnerFeeRate > maxPartnerFeeRate) { + maxPartnerFeeRate = partnerFeeRate; + } - emit FeeSplitUpdated(oldFeeRate, protocolFeeRate_); + emit TokenPartnerConfigUpdated(tokenAddress, partnerId, partnerFeeRate); } /** @@ -507,6 +526,10 @@ contract AgentTaxV2 is Initializable, AccessControlUpgradeable { 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; From 133244413f0de95650495ab1b74c632c2f9e6397 Mon Sep 17 00:00:00 2001 From: Weixiong Tay Date: Fri, 29 May 2026 16:58:00 +0800 Subject: [PATCH 3/3] fix: added safety check for partner id = 0 --- contracts/tax/AgentTaxV2.sol | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/contracts/tax/AgentTaxV2.sol b/contracts/tax/AgentTaxV2.sol index 897e617..7689caa 100644 --- a/contracts/tax/AgentTaxV2.sol +++ b/contracts/tax/AgentTaxV2.sol @@ -354,6 +354,10 @@ contract AgentTaxV2 is Initializable, AccessControlUpgradeable { if (partnerFee > 0) { address partnerRecipient = partnerRecipients[partnerConfig.partnerId]; + require( + partnerRecipient != address(0), + "Partner recipient not set" + ); IERC20(assetToken).safeTransfer(partnerRecipient, partnerFee); emit PartnerFeeDistributed( tokenAddress,