Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,6 @@ artifacts/
deployments
fireblocks_secret.key
.cursor/
env*
env*

lib/
25 changes: 21 additions & 4 deletions contracts/launchpadv2/MockUniswapV2Router02.sol
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "../pool/IUniswapV2Router02.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "../pool/IUniswapV2Factory.sol";

contract MockUniswapV2Router02 {
Expand Down Expand Up @@ -286,12 +286,29 @@ contract MockUniswapV2Router02 {

function swapExactTokensForTokensSupportingFeeOnTransferTokens(
uint amountIn,
uint amountOutMin,
uint,
address[] calldata path,
address to,
uint deadline
uint
) external {
// Mock implementation - do nothing
require(path.length == 2, "MockUniswapV2Router02: path");
IERC20 agent = IERC20(path[0]);
IERC20 quoteOut = IERC20(path[1]);
address pair = IUniswapV2Factory(_factory).getPair(path[0], path[1]);
// Match UniswapV2Router02 (sell → pair). No pair (e.g. bare MockERC20 in unit tests): use this contract.
address dest = pair != address(0) ? pair : address(this);

uint256 beforeBal = agent.balanceOf(dest);
require(
agent.transferFrom(msg.sender, dest, amountIn),
"MockUniswapV2Router02: pull agent"
);
uint256 receivedAgent = agent.balanceOf(dest) - beforeBal;
require(receivedAgent > 0, "MockUniswapV2Router02: zero in");
require(
quoteOut.transfer(to, receivedAgent),
"MockUniswapV2Router02: push quote"
);
}

function swapExactETHForTokensSupportingFeeOnTransferTokens(
Expand Down
14 changes: 14 additions & 0 deletions contracts/tax/AgentTaxV2.sol
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,20 @@ contract AgentTaxV2 is Initializable, AccessControlUpgradeable {
emit TaxDeposited(tokenAddress, amount);
}

function accountingTax(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);
}
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated

/**
* @notice Backend-triggered swap for a specific token's accumulated tax
* @dev Only backend can trigger swaps to ensure proper price verification.
Expand Down
78 changes: 78 additions & 0 deletions contracts/tax/TaxAccountingAdapter.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import "../pool/IUniswapV2Router02.sol";

interface IAgentTaxForToken {
function depositTax(address tokenAddress, uint256 amount) external;
}

/**
* @title TaxAccountingAdapter
* @notice Pulls agent tokens from the AgentToken contract, swaps to `pairToken` via Uniswap V2
* with `to = address(this)` so the pool's INVALID_TO check is satisfied (recipient is not token0/token1).
* Deposits the *actual* `pairToken` received into AgentTax via `depositTax(agentToken, received)`.
*/
contract TaxAccountingAdapter is ReentrancyGuard {
using SafeERC20 for IERC20;

event TaxSwapDeposited(
address indexed agentToken,
address indexed taxRecipient,
uint256 received
);

/**
* @param agentToken Agent ERC20 (same as `path[0]`). Tokens are pulled from `agentToken` address (the contract's own balance).
* @param pairToken Quote token (e.g. VIRTUAL).
* @param taxRecipient AgentTax (or compatible) — `depositTax` pulls `pairToken` from this adapter.
* @param router Uniswap V2 compatible router.
* @param swapAmount Amount of agent token to pull and pass to the router (fee-on-transfer: actual balance used after pull).
* @param deadline Router deadline.
*/
function swapTaxAndDeposit(
address agentToken,
address pairToken,
address taxRecipient,
address router,
uint256 swapAmount,
uint256 deadline
) external nonReentrant {
require(swapAmount > 0, "TaxAccountingAdapter: zero swap");
require(deadline >= block.timestamp, "TaxAccountingAdapter: expired");
require(
taxRecipient != address(0) && router != address(0),
"TaxAccountingAdapter: zero address"
);

IERC20(agentToken).safeTransferFrom(agentToken, address(this), swapAmount);

uint256 swapIn = IERC20(agentToken).balanceOf(address(this));
require(swapIn > 0, "TaxAccountingAdapter: zero in");

IERC20(agentToken).forceApprove(router, swapIn);

address[] memory path = new address[](2);
path[0] = agentToken;
path[1] = pairToken;

uint256 balBefore = IERC20(pairToken).balanceOf(address(this));

IUniswapV2Router02(router).swapExactTokensForTokensSupportingFeeOnTransferTokens(
swapIn,
0,
path,
address(this),
deadline
);

uint256 received = IERC20(pairToken).balanceOf(address(this)) - balBefore;
if (received > 0) {
IERC20(pairToken).forceApprove(taxRecipient, received);
IAgentTaxForToken(taxRecipient).depositTax(agentToken, received);
emit TaxSwapDeposited(agentToken, taxRecipient, received);
}
}
}
117 changes: 71 additions & 46 deletions contracts/virtualPersona/AgentTokenV3.sol
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,18 @@ interface IAgentTaxForToken {
function depositTax(address tokenAddress, uint256 amount) external;
}

/// @dev Optional helper: swap with `to = adapter` to satisfy Uniswap V2 Pair `INVALID_TO`, then `depositTax`.
interface ITaxAccountingAdapter {
function swapTaxAndDeposit(
address agentToken,
address pairToken,
address taxRecipient,
address router,
uint256 swapAmount,
uint256 deadline
) external;
}

contract AgentTokenV3 is
ContextUpgradeable,
IAgentTokenV3,
Expand Down Expand Up @@ -78,6 +90,9 @@ contract AgentTokenV3 is

mapping(address => bool) public blacklists;

/// @notice Required for autoswap: pulls tax, swaps with `to = adapter`, then `depositTax` on AgentTax.
address public taxAccountingAdapter;

/**
* @dev {onlyOwnerOrFactory}
*
Expand Down Expand Up @@ -465,6 +480,19 @@ contract AgentTokenV3 is
emit ProjectTaxRecipientUpdated(projectTaxRecipient_);
}

/**
* @notice Sets the adapter used by `_swapTax` (Uniswap `to` = adapter, not the agent token — avoids Pair INVALID_TO).
*/
function setTaxAccountingAdapter(
address adapter_
) external onlyOwnerOrFactory {
if (adapter_ == address(0)) {
revert CannotSetToZeroAddress();
}
taxAccountingAdapter = adapter_;
emit TaxAccountingAdapterUpdated(adapter_);
}

/**
* @dev function {setSwapThresholdBasisPoints} onlyOwnerOrFactory
*
Expand Down Expand Up @@ -919,58 +947,55 @@ contract AgentTokenV3 is
* @param contractBalance_ The current accumulated total tax balance
*/
function _swapTax(uint256 swapBalance_, uint256 contractBalance_) internal {
address[] memory path = new address[](2);
path[0] = address(this);
path[1] = pairToken;

uint256 balanceBefore = IERC20(pairToken).balanceOf(address(this));
if (taxAccountingAdapter == address(0)) {
revert TaxAccountingAdapterNotSet();
}
Comment thread
cursor[bot] marked this conversation as resolved.

// Wrap external calls in try / catch to handle errors
IERC20(address(this)).forceApprove(taxAccountingAdapter, swapBalance_);
try
_uniswapRouter
.swapExactTokensForTokensSupportingFeeOnTransferTokens(
swapBalance_,
0,
path,
address(this), // Receive to self first for on-chain attribution
block.timestamp + 600
)
ITaxAccountingAdapter(taxAccountingAdapter).swapTaxAndDeposit(
address(this),
pairToken,
projectTaxRecipient,
address(_uniswapRouter),
swapBalance_,
block.timestamp + 600
)
{
uint256 received = IERC20(pairToken).balanceOf(address(this)) - balanceBefore;

// Deposit tax with on-chain attribution (must succeed)
if (received > 0) {
IERC20(pairToken).forceApprove(projectTaxRecipient, received);
IAgentTaxForToken(projectTaxRecipient).depositTax(address(this), received);
}

// We will not have swapped all tax tokens IF the amount was greater than the max auto swap.
// We therefore cannot just set the pending swap counters to 0. Instead, in this scenario,
// we must reduce them in proportion to the swap amount vs the remaining balance + swap
// amount.
//
// For example:
// * swap Balance is 250
// * contract balance is 385.
// * projectTaxPendingSwap is 300
//
// The new total for the projectTaxPendingSwap is:
// = 300 - ((300 * 250) / 385)
// = 300 - 194
// = 106

if (swapBalance_ < contractBalance_) {
projectTaxPendingSwap -= uint128(
(projectTaxPendingSwap * swapBalance_) / contractBalance_
);
} else {
projectTaxPendingSwap = 0;
}
_applyProjectTaxPendingAfterSwap(swapBalance_, contractBalance_);
} catch {
// Dont allow a failed external call (in this case to uniswap) to stop a transfer.
// Emit that this has occured and continue.
// Dont allow a failed external call (adapter / uniswap / depositTax) to stop a transfer.
emit ExternalCallError(5);
}
IERC20(address(this)).forceApprove(taxAccountingAdapter, 0);
}

function _applyProjectTaxPendingAfterSwap(
uint256 swapBalance_,
uint256 contractBalance_
) private {
// We will not have swapped all tax tokens IF the amount was greater than the max auto swap.
// We therefore cannot just set the pending swap counters to 0. Instead, in this scenario,
// we must reduce them in proportion to the swap amount vs the remaining balance + swap
// amount.
//
// For example:
// * swap Balance is 250
// * contract balance is 385.
// * projectTaxPendingSwap is 300
//
// The new total for the projectTaxPendingSwap is:
// = 300 - ((300 * 250) / 385)
// = 300 - 194
// = 106

if (swapBalance_ < contractBalance_) {
projectTaxPendingSwap -= uint128(
(projectTaxPendingSwap * swapBalance_) / contractBalance_
);
} else {
projectTaxPendingSwap = 0;
}
}

/**
Expand Down
2 changes: 2 additions & 0 deletions contracts/virtualPersona/IAgentTokenV3.sol
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,6 @@ import "./IAgentTokenV2.sol";
interface IAgentTokenV3 is IAgentTokenV2 {
// AgentTokenV3 implements IAgentTokenV2 and adds on-chain tax attribution
// via depositTax() calls in _swapTax(). No additional external functions needed.

event TaxAccountingAdapterUpdated(address indexed adapter);
}
2 changes: 2 additions & 0 deletions contracts/virtualPersona/IErrors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -312,4 +312,6 @@ interface IErrors {
error VRFCoordinatorCannotBeAddressZero(); // The VRF coordinator cannot be the zero address (address(0)).

error TransferToBlacklistedAddress(); // Cannot transfer to a blacklisted address.

error TaxAccountingAdapterNotSet(); // Autoswap uses {TaxAccountingAdapter}; set before tax swaps can run.
}
Loading