Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
27 changes: 26 additions & 1 deletion contracts/launchpadv2/FPairV2.sol
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ contract FPairV2 is IFPairV2, ReentrancyGuard {

event TimeReset(uint256 oldStartTime, uint256 newStartTime);
event TaxStartTimeSet(uint256 taxStartTime);
event Sync(uint256 reserve0, uint256 reserve1);

modifier onlyRouter() {
require(router == msg.sender, "Only router can call this function");
Expand Down Expand Up @@ -135,6 +136,27 @@ contract FPairV2 is IFPairV2, ReentrancyGuard {
IERC20(tokenA).safeTransfer(recipient, amount);
}

/**
* @dev Sync reserves after drain operations to maintain state consistency
* Should be called after drainPrivatePool to update reserves
* @param assetAmount Amount of asset tokens (tokenB) transferred out
* @param tokenAmount Amount of agent tokens (tokenA) transferred out
*/
function syncAfterDrain(
uint256 assetAmount,
uint256 tokenAmount
) public onlyRouter {
// Subtract transferred amounts (don't use balanceOf due to virtual liquidity)
_pool.reserve0 = _pool.reserve0 >= tokenAmount
? _pool.reserve0 - tokenAmount
: 0;
_pool.reserve1 = _pool.reserve1 >= assetAmount
? _pool.reserve1 - assetAmount
: 0;
_pool.k = _pool.reserve0 * _pool.reserve1;
emit Sync(_pool.reserve0, _pool.reserve1);
}

function getReserves() public view returns (uint256, uint256) {
return (_pool.reserve0, _pool.reserve1);
}
Expand Down Expand Up @@ -175,7 +197,10 @@ contract FPairV2 is IFPairV2, ReentrancyGuard {

function setTaxStartTime(uint256 _taxStartTime) public onlyRouter {
// BE will input the _taxStartTime = time when call Launch(), so it's always after or at least equal to the startTime
require(_taxStartTime >= startTime, "Tax start time must be greater than startTime");
require(
_taxStartTime >= startTime,
"Tax start time must be greater than startTime"
);
taxStartTime = _taxStartTime;
emit TaxStartTimeSet(_taxStartTime);
}
Expand Down
154 changes: 154 additions & 0 deletions contracts/launchpadv2/FRouterV2.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,15 @@ import "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol
import "./FFactoryV2.sol";
import "./IFPairV2.sol";
import "../tax/IBondingTax.sol";
import "../virtualPersona/IAgentFactoryV6.sol";
import "../virtualPersona/IAgentVeTokenV2.sol";
import "../pool/IUniswapV2Pair.sol";

// Minimal interface for BondingV2 to avoid circular dependency
interface IBondingV2ForRouter {
function isProject60days(address token) external view returns (bool);
function agentFactory() external view returns (address);
}

contract FRouterV2 is
Initializable,
Expand All @@ -25,6 +34,21 @@ contract FRouterV2 is
address public assetToken;
address public taxManager; // deprecated
address public antiSniperTaxManager; // deprecated
IBondingV2ForRouter public bondingV2;

event PrivatePoolDrained(
address indexed token,
address indexed recipient,
uint256 assetAmount,
uint256 tokenAmount
);

event UniV2PoolDrained(
address indexed token,
address indexed veToken,
address indexed recipient,
uint256 veTokenAmount
);

/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
Expand Down Expand Up @@ -227,6 +251,15 @@ contract FRouterV2 is
antiSniperTaxManager = newManager;
}

/**
* @notice Set BondingV2 contract address for isProject60days check
* @param bondingV2_ The address of the BondingV2 contract
*/
function setBondingV2(address bondingV2_) public onlyRole(ADMIN_ROLE) {
require(bondingV2_ != address(0), "Invalid BondingV2 address");
bondingV2 = IBondingV2ForRouter(bondingV2_);
}

function resetTime(
address tokenAddress,
uint256 newStartTime
Expand Down Expand Up @@ -296,4 +329,125 @@ contract FRouterV2 is
// so old pair contract won't be called and thus no issue, but we just be safe here
}
}

// ==================== Liquidity Drain Functions ====================

/**
* @dev Drain all assets and tokens from a private pool (FPairV2)
* Only callable by EXECUTOR_ROLE and only for Project60days tokens
* @param tokenAddress The address of the fun token (must be isProject60days)
* @param recipient The address that will receive the drained assets and tokens
* @return assetAmount Amount of asset tokens drained
* @return tokenAmount Amount of agent tokens drained
*/
function drainPrivatePool(
address tokenAddress,
address recipient
) public onlyRole(EXECUTOR_ROLE) nonReentrant returns (uint256, uint256) {
require(address(bondingV2) != address(0), "BondingV2 not set");
require(tokenAddress != address(0), "Zero addresses are not allowed.");
require(recipient != address(0), "Zero addresses are not allowed.");

// Check isProject60days restriction
require(
bondingV2.isProject60days(tokenAddress),
"Token does not allow liquidity drain"
);

address pairAddress = factory.getPair(tokenAddress, assetToken);
require(pairAddress != address(0), "Pair not found");

IFPairV2 pair = IFPairV2(pairAddress);

uint256 assetAmount = pair.assetBalance();
uint256 tokenAmount = pair.balance();

if (assetAmount > 0) {
pair.transferAsset(recipient, assetAmount);
}
if (tokenAmount > 0) {
pair.transferTo(recipient, tokenAmount);
}

// Sync reserves after drain to maintain state consistency
// Use try-catch for backward compatibility with old FPairV2 contracts
try pair.syncAfterDrain(assetAmount, tokenAmount) {} catch {
// Old FPairV2 contracts don't have syncAfterDrain - drain still works,
// but reserves won't be synced (only affects getReserves() view function)
}
emit PrivatePoolDrained(
tokenAddress,
recipient,
assetAmount,
tokenAmount
);

return (assetAmount, tokenAmount);
}

/**
* @dev Drain ALL liquidity from a UniswapV2 pool (for graduated tokens)
* Only callable by EXECUTOR_ROLE and only for Project60days tokens
* @param agentToken The token address (same as agentToken in single token model, must be isProject60days)
* @param veToken The veToken address (staked LP token) to drain from
* @param recipient The address that will receive the drained liquidity
* @param deadline Transaction deadline
* @notice This function drains ALL liquidity (full founder balance)
* @notice amountAMin and amountBMin are set to 0 since this is a privileged drain operation
*/
function drainUniV2Pool(
address agentToken,
address veToken,
address recipient,
uint256 deadline
) public onlyRole(EXECUTOR_ROLE) nonReentrant {
require(address(bondingV2) != address(0), "BondingV2 not set");
require(agentToken != address(0), "Invalid agentToken");
require(veToken != address(0), "Invalid veToken");
require(recipient != address(0), "Invalid recipient");

// Check isProject60days restriction
require(
bondingV2.isProject60days(agentToken),
"agentToken does not allow liquidity drain"
);

// Verify veToken corresponds to the provided token
// veToken.assetToken() returns the LP pair address
address lpPair = IAgentVeTokenV2(veToken).assetToken();
IUniswapV2Pair pair = IUniswapV2Pair(lpPair);
address token0 = pair.token0();
address token1 = pair.token1();

require(
token0 == agentToken || token1 == agentToken,
"veToken does not match token"
);
require(
token0 == assetToken || token1 == assetToken, // assetToken is $Virtual
"veToken does not match assetToken"
);

// Get the FULL founder balance to drain ALL liquidity
IAgentVeTokenV2 veTokenContract = IAgentVeTokenV2(veToken);
address founder = veTokenContract.founder();
uint256 veTokenAmount = IERC20(veToken).balanceOf(founder);

require(veTokenAmount > 0, "No liquidity to drain");

// Call removeLpLiquidity through AgentFactoryV6
// amountAMin and amountBMin set to 0 - this is a privileged drain operation
// No slippage protection needed since EXECUTOR_ROLE is trusted
address agentFactory = bondingV2.agentFactory();
IAgentFactoryV6(agentFactory).removeLpLiquidity(
veToken,
recipient,
veTokenAmount,
0, // amountAMin - accept any amount
0, // amountBMin - accept any amount
deadline
);

emit UniV2PoolDrained(agentToken, veToken, recipient, veTokenAmount);
}
}
4 changes: 4 additions & 0 deletions contracts/launchpadv2/IFPairV2.sol
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,8 @@ interface IFPairV2 {
function setTaxStartTime(uint256 _taxStartTime) external;

function taxStartTime() external view returns (uint256);

function tokenA() external view returns (address);

function syncAfterDrain(uint256 assetAmount, uint256 tokenAmount) external;
}
3 changes: 2 additions & 1 deletion contracts/virtualPersona/AgentVeTokenV2.sol
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ contract AgentVeTokenV2 is
* @dev {onlyOwnerOrFactory}
*
* Throws if called by any account other than the owner, factory or pool.
* owner has not been set yet, _factory = agentFactoryV6
*/
modifier onlyOwnerOrFactory() {
if (owner() != _msgSender() && address(_factory) != _msgSender()) {
Expand Down Expand Up @@ -161,7 +162,7 @@ contract AgentVeTokenV2 is

/**
* @dev Removes liquidity from Uniswap V2 pair and burns corresponding staked LP tokens
* Only callable by admin
* Only callable by admin, draining rugged Project60days (intentionally BYPASSES matureAt)
*
* @param uniswapRouter The address of the Uniswap V2 router
* @param veTokenAmount The amount of veToken (underlying lpToken) to remove liquidity for
Expand Down
9 changes: 9 additions & 0 deletions contracts/virtualPersona/IAgentFactoryV6.sol
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,13 @@ interface IAgentFactoryV6 {
function addBlacklistAddress(address token, address blacklistAddress) external;

function removeBlacklistAddress(address token, address blacklistAddress) external;

function removeLpLiquidity(
address veToken,
address recipient,
uint256 veTokenAmount,
uint256 amountAMin,
uint256 amountBMin,
uint256 deadline
) external;
}
11 changes: 10 additions & 1 deletion contracts/virtualPersona/IAgentVeTokenV2.sol
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,16 @@ interface IAgentVeTokenV2 is IAgentVeToken {
uint256 timepoint
) external view returns (uint256);

function removeLpLiquidity(address uniswapRouter, uint256 veTokenAmount, address recipient, uint256 amountAMin, uint256 amountBMin, uint256 deadline) external;
function removeLpLiquidity(
address uniswapRouter,
uint256 veTokenAmount,
address recipient,
uint256 amountAMin,
uint256 amountBMin,
uint256 deadline
) external;

function assetToken() external view returns (address);

function founder() external view returns (address);
}
Loading