Address
0x8aa799489e976d003175cbf835fc49e91ca8d5eaCurrent Holdings
$0.00
Total TXs
7
sent 0 + received 7
First Active
2026-09-15
block 27,554,259
Last Active
1 day ago
block 27,556,316
Funded By
not identified
Net worth historyi
No net-worth snapshots recorded yet
exact matchTokenFactorysolc 0.8.34+commit.80d5c536runtime exact · creation exact
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
abstract contract Context {
function _msgSender() internal view virtual returns (address) {
return msg.sender;
}
}
interface IERC20 {
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
function totalSupply() external view returns (uint256);
function balanceOf(address account) external view returns (uint256);
function transfer(address to, uint256 value) external returns (bool);
function allowance(address owner, address spender) external view returns (uint256);
function approve(address spender, uint256 value) external returns (bool);
function transferFrom(address from, address to, uint256 value) external returns (bool);
}
interface IERC20Metadata is IERC20 {
function name() external view returns (string memory);
function symbol() external view returns (string memory);
function decimals() external view returns (uint8);
}
interface IERC20Errors {
error ERC20InsufficientBalance(address sender, uint256 balance, uint256 needed);
error ERC20InvalidSender(address sender);
error ERC20InvalidReceiver(address receiver);
error ERC20InsufficientAllowance(address spender, uint256 allowance, uint256 needed);
error ERC20InvalidApprover(address approver);
error ERC20InvalidSpender(address spender);
}
/// @dev Minimal UniswapV2Router02-compatible interface. PulseX e a maioria
/// dos forks da PulseChain (incl. NineInch) implementam essa mesma forma,
/// mantendo o nome WPLS(). Confira contra o router real do DEX alvo antes
/// de implantar.
interface IDexRouter02 {
function factory() external pure returns (address);
function WPLS() external pure returns (address);
function swapExactETHForTokens(
uint256 amountOutMin,
address[] calldata path,
address to,
uint256 deadline
) external payable returns (uint256[] memory amounts);
function addLiquidity(
address tokenA,
address tokenB,
uint256 amountADesired,
uint256 amountBDesired,
uint256 amountAMin,
uint256 amountBMin,
address to,
uint256 deadline
) external returns (uint256 amountA, uint256 amountB, uint256 liquidity);
function addLiquidityETH(
address token,
uint256 amountTokenDesired,
uint256 amountTokenMin,
uint256 amountETHMin,
address to,
uint256 deadline
) external payable returns (uint256 amountToken, uint256 amountETH, uint256 liquidity);
function swapExactETHForTokensSupportingFeeOnTransferTokens(
uint256 amountOutMin,
address[] calldata path,
address to,
uint256 deadline
) external payable;
function swapExactTokensForTokensSupportingFeeOnTransferTokens(
uint256 amountIn,
uint256 amountOutMin,
address[] calldata path,
address to,
uint256 deadline
) external;
function swapExactTokensForETHSupportingFeeOnTransferTokens(
uint256 amountIn,
uint256 amountOutMin,
address[] calldata path,
address to,
uint256 deadline
) external;
}
interface IDexFactory {
function getPair(address tokenA, address tokenB) external view returns (address pair);
}
/// @dev Vista mínima da TokenFactory usada pelo Token pra ler o owner ("father")
/// e o dexRouter corrente no momento da migração (em vez de guardar uma cópia
/// imutável — assim, se a factory trocar de router via setDexRouter() antes de
/// um token específico migrar, a migração usa o router atualizado).
interface ITokenFactory {
function owner() external view returns (address);
function dexRouter() external view returns (IDexRouter02);
}
abstract contract ERC20 is Context, IERC20, IERC20Metadata, IERC20Errors {
mapping(address => uint256) private _balances;
mapping(address => mapping(address => uint256)) private _allowances;
uint256 private _totalSupply;
string private _name;
string private _symbol;
constructor(string memory name_, string memory symbol_) {
_name = name_;
_symbol = symbol_;
}
function name() public view virtual returns (string memory) { return _name; }
function symbol() public view virtual returns (string memory) { return _symbol; }
function decimals() public view virtual returns (uint8) { return 18; }
function totalSupply() public view virtual returns (uint256) { return _totalSupply; }
function balanceOf(address account) public view virtual returns (uint256) { return _balances[account]; }
function transfer(address to, uint256 value) public virtual returns (bool) {
_transfer(_msgSender(), to, value);
return true;
}
function allowance(address owner, address spender) public view virtual returns (uint256) {
return _allowances[owner][spender];
}
function approve(address spender, uint256 value) public virtual returns (bool) {
_approve(_msgSender(), spender, value);
return true;
}
function transferFrom(address from, address to, uint256 value) public virtual returns (bool) {
_spendAllowance(from, _msgSender(), value);
_transfer(from, to, value);
return true;
}
function _transfer(address from, address to, uint256 value) internal virtual {
if (from == address(0)) revert ERC20InvalidSender(address(0));
if (to == address(0)) revert ERC20InvalidReceiver(address(0));
_update(from, to, value);
}
function _update(address from, address to, uint256 value) internal virtual {
if (from == address(0)) {
_totalSupply += value;
} else {
uint256 fromBalance = _balances[from];
if (fromBalance < value) revert ERC20InsufficientBalance(from, fromBalance, value);
unchecked { _balances[from] = fromBalance - value; }
}
if (to == address(0)) {
unchecked { _totalSupply -= value; }
} else {
unchecked { _balances[to] += value; }
}
emit Transfer(from, to, value);
}
function _mint(address account, uint256 value) internal {
if (account == address(0)) revert ERC20InvalidReceiver(address(0));
_update(address(0), account, value);
}
function _approve(address owner, address spender, uint256 value) internal {
_approve(owner, spender, value, true);
}
function _approve(address owner, address spender, uint256 value, bool emitEvent) internal virtual {
if (owner == address(0)) revert ERC20InvalidApprover(address(0));
if (spender == address(0)) revert ERC20InvalidSpender(address(0));
_allowances[owner][spender] = value;
if (emitEvent) emit Approval(owner, spender, value);
}
function _spendAllowance(address owner, address spender, uint256 value) internal virtual {
uint256 currentAllowance = allowance(owner, spender);
if (currentAllowance != type(uint256).max) {
if (currentAllowance < value) revert ERC20InsufficientAllowance(spender, currentAllowance, value);
unchecked { _approve(owner, spender, currentAllowance - value, false); }
}
}
}
abstract contract Ownable is Context {
address private _owner;
error OwnableUnauthorizedAccount(address account);
error OwnableInvalidOwner(address owner);
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
constructor(address initialOwner) {
if (initialOwner == address(0)) revert OwnableInvalidOwner(address(0));
_transferOwnership(initialOwner);
}
modifier onlyOwner() { _checkOwner(); _; }
function owner() public view virtual returns (address) { return _owner; }
function _checkOwner() internal view virtual {
if (owner() != _msgSender()) revert OwnableUnauthorizedAccount(_msgSender());
}
function transferOwnership(address newOwner) public virtual onlyOwner {
if (newOwner == address(0)) revert OwnableInvalidOwner(address(0));
_transferOwnership(newOwner);
}
function _transferOwnership(address newOwner) internal virtual {
address oldOwner = _owner;
_owner = newOwner;
emit OwnershipTransferred(oldOwner, newOwner);
}
}
abstract contract ReentrancyGuard {
uint256 private constant _NOT_ENTERED = 1;
uint256 private constant _ENTERED = 2;
uint256 private _status = _NOT_ENTERED;
error ReentrancyGuardReentrantCall();
modifier nonReentrant() {
if (_status == _ENTERED) revert ReentrancyGuardReentrantCall();
_status = _ENTERED;
_;
_status = _NOT_ENTERED;
}
}
/* ------------------------------------------------------------------------ */
/* QuoteRegistry */
/* ------------------------------------------------------------------------ */
error QuoteRegistry_AlreadyExists();
error QuoteRegistry_NotFound();
error QuoteRegistry_ZeroAddress();
error QuoteRegistry_InvalidNativeFlag();
/// @notice Allowlist de ativos de cotação que um token pode usar como par —
/// tanto PLS nativo (isNative = true, token = address(0)) quanto qualquer
/// ERC20 (DAI, USDT, etc). Cada quote carrega seus próprios parâmetros de
/// curva (reservas virtuais iniciais) e seu próprio threshold de migração,
/// então quotes com escalas de valor bem diferentes podem conviver sem
/// precisar de lógica especial no Token.
contract QuoteRegistry is Ownable {
struct QuoteInfo {
address token; // endereço do quote-token ERC20, ou address(0) se isNative
bool isNative; // true = par nativo em PLS (sem quote-token ERC20)
uint8 decimals;
uint256 migrationThreshold; // quanto de quote REAL (unidades brutas) acumulado na curva dispara a migração
uint256 virtualQuoteReserveInit; // reserva virtual inicial de quote -- define preço inicial / curvatura
uint256 virtualTokenReserveInit; // reserva virtual inicial de token -- define preço inicial / curvatura
string label;
bool active;
}
mapping(bytes32 => QuoteInfo) private _quotes;
mapping(bytes32 => bool) private _exists;
bytes32[] public quoteIds;
event QuoteAdded(bytes32 indexed id, address indexed token, bool isNative, string label);
event QuoteUpdated(bytes32 indexed id);
event QuoteActiveSet(bytes32 indexed id, bool active);
constructor(address initialOwner) Ownable(initialOwner) {}
function addQuote(
bytes32 id,
address token,
bool isNative,
uint8 tokenDecimals,
uint256 migrationThreshold,
uint256 virtualQuoteReserveInit,
uint256 virtualTokenReserveInit,
string calldata label
) external onlyOwner {
if (isNative && token != address(0)) revert QuoteRegistry_InvalidNativeFlag();
if (!isNative && token == address(0)) revert QuoteRegistry_ZeroAddress();
if (_exists[id]) revert QuoteRegistry_AlreadyExists();
_quotes[id] = QuoteInfo({
token: token,
isNative: isNative,
decimals: tokenDecimals,
migrationThreshold: migrationThreshold,
virtualQuoteReserveInit: virtualQuoteReserveInit,
virtualTokenReserveInit: virtualTokenReserveInit,
label: label,
active: true
});
_exists[id] = true;
quoteIds.push(id);
emit QuoteAdded(id, token, isNative, label);
}
function setActive(bytes32 id, bool active) external onlyOwner {
if (!_exists[id]) revert QuoteRegistry_NotFound();
_quotes[id].active = active;
emit QuoteActiveSet(id, active);
}
function updateCurveParams(
bytes32 id,
uint256 migrationThreshold,
uint256 virtualQuoteReserveInit,
uint256 virtualTokenReserveInit
) external onlyOwner {
if (!_exists[id]) revert QuoteRegistry_NotFound();
QuoteInfo storage q = _quotes[id];
q.migrationThreshold = migrationThreshold;
q.virtualQuoteReserveInit = virtualQuoteReserveInit;
q.virtualTokenReserveInit = virtualTokenReserveInit;
emit QuoteUpdated(id);
}
function updateLabel(bytes32 id, string calldata label) external onlyOwner {
if (!_exists[id]) revert QuoteRegistry_NotFound();
_quotes[id].label = label;
emit QuoteUpdated(id);
}
function getQuote(bytes32 id) external view returns (QuoteInfo memory) {
if (!_exists[id]) revert QuoteRegistry_NotFound();
return _quotes[id];
}
function allQuoteIds() external view returns (bytes32[] memory) {
return quoteIds;
}
}
/* ------------------------------------------------------------------------ */
/* Token */
/* ------------------------------------------------------------------------ */
error Token_NotFactory();
error Token_ZeroAddress();
error Token_CurveNotActive();
error Token_CurveClosed();
error Token_DeadlineExpired();
error Token_ZeroAmount();
error Token_SlippageExceeded();
error Token_UnexpectedValue();
error Token_CurveSupplyExceeded();
error Token_AlreadyMigrated();
error Token_ThresholdNotReached();
error Token_TransferFailed();
error Token_TransfersLockedDuringCurve();
error Token_InsufficientCurveLiquidity();
error Token_OnlySelf();
contract Token is ERC20, ReentrancyGuard {
uint256 public constant INITIAL_SUPPLY = 1_000_000_000 * 10 ** 18;
address public constant DEAD = 0x000000000000000000000000000000000000dEaD;
address public constant NATIVE = address(0);
// --- alocação da curva ---
// 80% do supply é vendável na curva; os 20% restantes ficam travados no
// contrato até a migração. Na migração, TODO o saldo restante do
// contrato (>= MIGRATION_TOKEN_RESERVE_MIN, pode ser mais se o threshold
// bater antes de esgotar a alocação da curva) vira liquidez -- assim
// nenhum token fica preso pra sempre no contrato.
uint256 public constant CURVE_TOKEN_ALLOCATION = 800_000_000 * 10 ** 18;
uint256 public constant MIGRATION_TOKEN_RESERVE_MIN = 200_000_000 * 10 ** 18;
// ---- taxas, em pontos-base de 10_000 ----
uint16 public constant BUY_FATHER_FEE_BP = 70; // 0.7% -> owner da factory ("father")
uint16 public constant BUY_CREATOR_FEE_BP = 30; // 0.3% -> criador do token
uint16 public constant SELL_CREATOR_FEE_BP = 30; // 0.3% -> criador do token (sem taxa de father na venda)
address public immutable factory;
address public immutable creator;
address public immutable quoteToken; // address(0) = par nativo em PLS
// --- estado da curva ---
bool public curveActive; // true = fase 1 (curva), false = fase 2 (DEX)
bool public migrated; // trava _migrate() pra rodar uma única vez
uint256 public virtualQuoteReserve; // reserva virtual de quote (não é saldo real, é parâmetro da curva)
uint256 public virtualTokenReserve; // reserva virtual de tokens (idem)
uint256 public realQuoteReserve; // saldo REAL de quote acumulado das compras na curva
uint256 public curveTokensSold; // total de tokens já entregues (bruto, antes de taxa) na curva
uint256 public immutable migrationThreshold; // quanto de quote real precisa acumular pra migrar
address public pair;
event Launched(address indexed pair, uint256 tokenLiquidity, uint256 quoteLiquidity);
event FeeTaken(bool indexed isBuy, address indexed to, uint256 amount);
event CurveBuy(address indexed buyer, address indexed to, uint256 quoteIn, uint256 tokensOut);
event CurveSell(address indexed seller, address indexed to, uint256 tokensIn, uint256 quoteOut);
modifier onlyFactory() {
if (_msgSender() != factory) revert Token_NotFactory();
_;
}
constructor(
string memory _name,
string memory _symbol,
address _creator,
address _factory,
address _quoteToken, // address(0) = par nativo em PLS
uint256 _virtualQuoteReserveInit,
uint256 _virtualTokenReserveInit,
uint256 _migrationThreshold
) ERC20(_name, _symbol) {
if (_factory == address(0) || _creator == address(0)) revert Token_ZeroAddress();
if (_virtualQuoteReserveInit == 0 || _virtualTokenReserveInit == 0) revert Token_ZeroAmount();
factory = _factory;
creator = _creator;
quoteToken = _quoteToken;
virtualQuoteReserve = _virtualQuoteReserveInit;
virtualTokenReserve = _virtualTokenReserveInit;
migrationThreshold = _migrationThreshold;
curveActive = true;
// todo o supply nasce dentro do próprio contrato: 80% vendável na
// curva, 20% travado até a migração. Nada de "launch()" separado --
// a curva já nasce ativa.
_mint(address(this), INITIAL_SUPPLY);
}
function mint(address to, uint256 amount) external onlyFactory {
_mint(to, amount);
}
/* ------------------------------ curva --------------------------------- */
/// @param amountIn Quantidade de quote-token ERC20 a gastar. Ignorado
/// (use msg.value) se quoteToken == NATIVE.
/// @param minTokensOut Proteção de slippage.
/// @param to Endereço que recebe os tokens comprados (permite roteamento
/// via SwapRouter em nome do usuário final).
function buyOnCurve(
uint256 amountIn,
uint256 minTokensOut,
address to,
uint256 deadline
) external payable nonReentrant returns (uint256 tokensOut) {
if (!curveActive) revert Token_CurveNotActive();
if (block.timestamp > deadline) revert Token_DeadlineExpired();
if (to == address(0)) revert Token_ZeroAddress();
address payer = _msgSender();
uint256 quoteIn;
if (quoteToken == NATIVE) {
quoteIn = msg.value;
if (quoteIn == 0) revert Token_ZeroAmount();
} else {
if (msg.value != 0) revert Token_UnexpectedValue();
quoteIn = amountIn;
if (quoteIn == 0) revert Token_ZeroAmount();
IERC20(quoteToken).transferFrom(payer, address(this), quoteIn);
}
// produto constante virtual: dx * dy = k
uint256 grossTokensOut = virtualTokenReserve -
(virtualQuoteReserve * virtualTokenReserve) / (virtualQuoteReserve + quoteIn);
if (curveTokensSold + grossTokensOut > CURVE_TOKEN_ALLOCATION) revert Token_CurveSupplyExceeded();
uint256 fatherFee = (grossTokensOut * BUY_FATHER_FEE_BP) / 10_000;
uint256 creatorFee = (grossTokensOut * BUY_CREATOR_FEE_BP) / 10_000;
tokensOut = grossTokensOut - fatherFee - creatorFee;
if (tokensOut < minTokensOut) revert Token_SlippageExceeded();
virtualQuoteReserve += quoteIn;
virtualTokenReserve -= grossTokensOut;
realQuoteReserve += quoteIn;
curveTokensSold += grossTokensOut;
address fatherAddr = ITokenFactory(factory).owner();
if (fatherFee > 0) {
_update(address(this), fatherAddr, fatherFee);
emit FeeTaken(true, fatherAddr, fatherFee);
}
if (creatorFee > 0) {
_update(address(this), creator, creatorFee);
emit FeeTaken(true, creator, creatorFee);
}
_update(address(this), to, tokensOut);
emit CurveBuy(payer, to, quoteIn, tokensOut);
if (realQuoteReserve >= migrationThreshold) {
// Tenta migrar automaticamente dentro da própria compra. Isso é
// feito via chamada externa (this._selfMigrate) dentro de um
// try/catch de propósito: se o addLiquidity[ETH] no DEX reverter
// por qualquer motivo (router mal configurado, pool com
// problema, etc.), a COMPRA continua valendo -- o comprador
// recebe os tokens normalmente e a migração fica pendente,
// podendo ser destravada depois por qualquer um via migrate().
// Antes dessa mudança, uma falha na migração revertia a compra
// inteira que cruzou o threshold, bloqueando exatamente a
// compra que deveria fechar a curva.
try this._selfMigrate() {} catch {}
}
}
/// @param tokenAmountIn Quantidade de tokens (do próprio caller) a vender.
/// @param to Endereço que recebe o quote de saída (nativo ou ERC20).
function sellOnCurve(
uint256 tokenAmountIn,
uint256 minQuoteOut,
address to,
uint256 deadline
) external nonReentrant returns (uint256 quoteOut) {
if (!curveActive) revert Token_CurveNotActive();
if (block.timestamp > deadline) revert Token_DeadlineExpired();
if (tokenAmountIn == 0) revert Token_ZeroAmount();
if (to == address(0)) revert Token_ZeroAddress();
address seller = _msgSender();
uint256 creatorFee = (tokenAmountIn * SELL_CREATOR_FEE_BP) / 10_000;
uint256 tokenAmountInAfterFee = tokenAmountIn - creatorFee;
quoteOut = virtualQuoteReserve -
(virtualQuoteReserve * virtualTokenReserve) / (virtualTokenReserve + tokenAmountInAfterFee);
if (quoteOut < minQuoteOut) revert Token_SlippageExceeded();
if (quoteOut > realQuoteReserve) revert Token_InsufficientCurveLiquidity();
virtualTokenReserve += tokenAmountInAfterFee;
virtualQuoteReserve -= quoteOut;
realQuoteReserve -= quoteOut;
curveTokensSold -= tokenAmountInAfterFee > curveTokensSold ? curveTokensSold : tokenAmountInAfterFee;
// puxa os tokens do vendedor via _update interno -- bypassa o lock de
// _transfer (que só se aplica a transfer()/transferFrom() públicos).
_update(seller, address(this), tokenAmountInAfterFee);
if (creatorFee > 0) {
_update(seller, creator, creatorFee);
emit FeeTaken(false, creator, creatorFee);
}
_payoutQuote(to, quoteOut);
emit CurveSell(seller, to, tokenAmountIn, quoteOut);
}
/// @notice Fallback permissionless: qualquer um pode disparar a migração
/// se o threshold já foi atingido mas a auto-chamada dentro de
/// buyOnCurve não rodou (ex: falha de gas naquela tx específica).
function migrate() external nonReentrant {
if (!curveActive) revert Token_CurveNotActive();
if (realQuoteReserve < migrationThreshold) revert Token_ThresholdNotReached();
_migrate();
}
/// @dev Só pode ser chamado pelo próprio contrato, via `try this._selfMigrate()`
/// dentro de buyOnCurve. Não leva o modifier nonReentrant de propósito:
/// já estamos dentro de uma chamada protegida por nonReentrant
/// (buyOnCurve), então reentrância já está bloqueada nesse ponto --
/// colocar o modifier aqui faria essa chamada sempre reverter com
/// ReentrancyGuardReentrantCall, e o catch engoliria isso silenciosamente
/// (a migração nunca aconteceria pela compra, só pelo migrate() manual).
function _selfMigrate() external {
if (msg.sender != address(this)) revert Token_OnlySelf();
_migrate();
}
function _migrate() internal {
if (!curveActive || migrated) revert Token_AlreadyMigrated();
curveActive = false;
migrated = true;
uint256 quoteForLiquidity = realQuoteReserve;
realQuoteReserve = 0;
// todo o saldo de tokens que sobrou no contrato vira liquidez --
// nunca menos que MIGRATION_TOKEN_RESERVE_MIN, pode ser mais se o
// threshold bateu antes de esgotar CURVE_TOKEN_ALLOCATION.
uint256 tokensForLiquidity = balanceOf(address(this));
IDexRouter02 dexRouter = ITokenFactory(factory).dexRouter();
address newPair;
_approve(address(this), address(dexRouter), tokensForLiquidity);
if (quoteToken == NATIVE) {
dexRouter.addLiquidityETH{value: quoteForLiquidity}(
address(this),
tokensForLiquidity,
0,
0,
DEAD, // LP queimada permanentemente
block.timestamp
);
newPair = IDexFactory(dexRouter.factory()).getPair(address(this), dexRouter.WPLS());
} else {
IERC20(quoteToken).approve(address(dexRouter), quoteForLiquidity);
dexRouter.addLiquidity(
address(this),
quoteToken,
tokensForLiquidity,
quoteForLiquidity,
0,
0,
DEAD, // LP queimada permanentemente
block.timestamp
);
newPair = IDexFactory(dexRouter.factory()).getPair(address(this), quoteToken);
}
pair = newPair;
emit Launched(newPair, tokensForLiquidity, quoteForLiquidity);
}
function _payoutQuote(address to, uint256 amount) internal {
if (quoteToken == NATIVE) {
(bool success, ) = payable(to).call{value: amount}("");
if (!success) revert Token_TransferFailed();
} else {
IERC20(quoteToken).transfer(to, amount);
}
}
/* ---------------------------- fee logic ------------------------------ */
function _transfer(address from, address to, uint256 value) internal override {
// durante a fase de curva, transferências públicas (transfer /
// transferFrom) ficam travadas -- só buyOnCurve/sellOnCurve movem
// saldo, via _update interno, que não passa por aqui. Isso evita um
// mercado secundário paralelo fora da curva antes da migração.
if (curveActive) revert Token_TransfersLockedDuringCurve();
super._transfer(from, to, value);
}
function _update(address from, address to, uint256 value) internal override {
if (migrated && pair != address(0) && value > 0) {
if (from == pair) {
// COMPRA: pair -> destinatário
uint256 fatherFee = (value * BUY_FATHER_FEE_BP) / 10_000;
uint256 creatorFee = (value * BUY_CREATOR_FEE_BP) / 10_000;
uint256 amountAfterFee = value - fatherFee - creatorFee;
address fatherAddr = ITokenFactory(factory).owner();
if (fatherFee > 0) {
super._update(from, fatherAddr, fatherFee);
emit FeeTaken(true, fatherAddr, fatherFee);
}
if (creatorFee > 0) {
super._update(from, creator, creatorFee);
emit FeeTaken(true, creator, creatorFee);
}
super._update(from, to, amountAfterFee);
return;
} else if (to == pair) {
// VENDA: remetente -> pair
uint256 creatorFee = (value * SELL_CREATOR_FEE_BP) / 10_000;
uint256 amountAfterFee = value - creatorFee;
if (creatorFee > 0) {
super._update(from, creator, creatorFee);
emit FeeTaken(false, creator, creatorFee);
}
super._update(from, to, amountAfterFee);
return;
}
}
super._update(from, to, value);
}
receive() external payable {}
}
/* ------------------------------------------------------------------------ */
/* Factory */
/* ------------------------------------------------------------------------ */
error Factory_QuoteNotActive();
error Factory_NotFromFactory();
error Factory_NotCreator();
error Factory_NoDefaultQuote();
contract TokenFactory is Ownable {
struct TokenInfo {
address creator;
bytes32 quoteId;
address quoteToken;
string description;
string twitter;
string telegram;
string website;
string image;
}
// Sem swap de seed, sem compra especial pro criador, sem launch()
// imediato -- create() só faz o deploy. A curva já nasce ativa dentro
// do próprio Token. Se o criador quiser tokens, ele chama
// Token.buyOnCurve() como qualquer outro comprador, sem tratamento
// especial nem fatia reservada.
struct CreateParams {
string name;
string symbol;
bytes32 quoteId;
string description;
string twitter;
string telegram;
string website;
string image;
}
event TokenCreated(
address indexed token,
address indexed creator,
bytes32 indexed quoteId,
address quoteToken,
string name,
string symbol
);
event TokenInfoUpdated(address indexed token);
event DexRouterUpdated(address indexed oldRouter, address indexed newRouter);
event DefaultQuoteIdUpdated(bytes32 indexed oldId, bytes32 indexed newId);
QuoteRegistry public immutable quoteRegistry;
IDexRouter02 public dexRouter;
// Quote usada quando o criador não escolhe nenhuma explicitamente
// (p.quoteId == bytes32(0) em create()). Pareamento com um token
// específico (DAI, USDT etc.) vira opcional -- por padrão o token nasce
// pareado com essa quote (tipicamente a nativa, PLS). O owner define
// qual é via setDefaultQuoteId().
bytes32 public defaultQuoteId;
address[] public allTokens;
mapping(address => bool) public isTokenFromFactory;
mapping(address => TokenInfo) public tokenInfo;
constructor(address _quoteRegistry, address _dexRouter, bytes32 _defaultQuoteId) Ownable(msg.sender) {
quoteRegistry = QuoteRegistry(_quoteRegistry);
dexRouter = IDexRouter02(_dexRouter);
defaultQuoteId = _defaultQuoteId;
}
function setDexRouter(address newRouter) external onlyOwner {
emit DexRouterUpdated(address(dexRouter), newRouter);
dexRouter = IDexRouter02(newRouter);
}
/// @notice Define qual quote é usada quando o criador não escolhe
/// nenhuma em create() (quoteId == bytes32(0)). Precisa já existir e
/// estar ativa no QuoteRegistry.
function setDefaultQuoteId(bytes32 newId) external onlyOwner {
QuoteRegistry.QuoteInfo memory q = quoteRegistry.getQuote(newId); // reverte se não existir
if (!q.active) revert Factory_QuoteNotActive();
emit DefaultQuoteIdUpdated(defaultQuoteId, newId);
defaultQuoteId = newId;
}
/// @notice Deploy simples do token. A curva já nasce ativa, configurada
/// com os parâmetros do quote escolhido. Nenhum PLS/valor é recebido
/// aqui -- compras acontecem depois, direto em Token.buyOnCurve() (ou via
/// SwapRouter.buy()).
function create(CreateParams calldata p) external returns (address tokenAddr) {
// Pareamento com um quote específico é opcional: se o criador não
// passar quoteId (deixar bytes32(0)), usa a quote padrão da
// factory (defaultQuoteId, definida por setDefaultQuoteId()).
bytes32 quoteIdToUse = p.quoteId == bytes32(0) ? defaultQuoteId : p.quoteId;
if (quoteIdToUse == bytes32(0)) revert Factory_NoDefaultQuote();
QuoteRegistry.QuoteInfo memory q = quoteRegistry.getQuote(quoteIdToUse);
if (!q.active) revert Factory_QuoteNotActive();
Token token = new Token(
p.name,
p.symbol,
_msgSender(),
address(this),
q.token,
q.virtualQuoteReserveInit,
q.virtualTokenReserveInit,
q.migrationThreshold
);
tokenAddr = address(token);
_recordToken(tokenAddr, p, quoteIdToUse, q.token);
}
function mintFor(address token, address to, uint256 amount) external onlyOwner {
require(isTokenFromFactory[token], "token nao veio deste factory");
Token(payable(token)).mint(to, amount);
}
function _recordToken(
address tokenAddr,
CreateParams calldata p,
bytes32 quoteIdUsed,
address quoteTokenAddr
) internal {
isTokenFromFactory[tokenAddr] = true;
allTokens.push(tokenAddr);
tokenInfo[tokenAddr] = TokenInfo({
creator: _msgSender(),
quoteId: quoteIdUsed,
quoteToken: quoteTokenAddr,
description: p.description,
twitter: p.twitter,
telegram: p.telegram,
website: p.website,
image: p.image
});
emit TokenCreated(tokenAddr, _msgSender(), quoteIdUsed, quoteTokenAddr, p.name, p.symbol);
}
function updateTokenInfo(
address token,
string calldata description,
string calldata twitter,
string calldata telegram,
string calldata website,
string calldata image
) external {
if (!isTokenFromFactory[token]) revert Factory_NotFromFactory();
if (tokenInfo[token].creator != _msgSender()) revert Factory_NotCreator();
TokenInfo storage info = tokenInfo[token];
info.description = description;
info.twitter = twitter;
info.telegram = telegram;
info.website = website;
info.image = image;
emit TokenInfoUpdated(token);
}
function totalTokensCreated() external view returns (uint256) { return allTokens.length; }
function getAllTokens() external view returns (address[] memory) { return allTokens; }
function getTokenInfo(address token) external view returns (TokenInfo memory) { return tokenInfo[token]; }
}
/* ------------------------------------------------------------------------ */
/* SwapRouter */
/* ------------------------------------------------------------------------ */
error SwapRouter_UnsupportedInput();
error SwapRouter_UnsupportedOutput();
error SwapRouter_BadValue();
error SwapRouter_SellDuringCurveUnsupported();
/// @notice Entrypoint de negociação voltado pro usuário. `address(0)` é a
/// convenção pra "PLS nativo" tanto do lado de entrada (compra) quanto de
/// saída (venda) -- inclusive quando o próprio par do token É nativo.
///
/// IMPORTANTE: durante a fase de curva, sell() não funciona -- o token trava
/// transferências públicas nessa fase (ver Token._transfer), então o router
/// não consegue puxar o saldo do usuário via transferFrom. Pra vender
/// durante a curva, chame Token.sellOnCurve() diretamente. buy() funciona
/// normalmente nas duas fases, porque só movimenta o quote-token (ou PLS
/// nativo), nunca o token da curva em si.
contract SwapRouter is ReentrancyGuard {
address public constant NATIVE = address(0);
TokenFactory public immutable factory;
constructor(address _factory) {
factory = TokenFactory(_factory);
}
/// @param inputToken NATIVE (paga com PLS) ou o próprio quoteToken do token (paga com o ERC20 quote direto)
function buy(
address token,
uint256 amountIn,
uint256 minOut,
address inputToken,
uint256 deadline
) external payable nonReentrant {
address quote = Token(payable(token)).quoteToken();
bool curveActive = Token(payable(token)).curveActive();
if (curveActive) {
if (quote == NATIVE) {
// curva nativa: só aceita PLS diretamente, sem conversão --
// não existe pool ainda pra converter outro ativo em PLS.
if (inputToken != NATIVE) revert SwapRouter_UnsupportedInput();
if (msg.value == 0) revert SwapRouter_BadValue();
Token(payable(token)).buyOnCurve{value: msg.value}(msg.value, minOut, _msgSender(), deadline);
} else if (inputToken == quote) {
// curva em quote-token ERC20, pagando com o próprio quote-token.
if (msg.value != 0) revert SwapRouter_BadValue();
IERC20(quote).transferFrom(_msgSender(), address(this), amountIn);
IERC20(quote).approve(token, amountIn);
Token(payable(token)).buyOnCurve(amountIn, minOut, _msgSender(), deadline);
} else if (inputToken == NATIVE) {
// curva em quote-token ERC20, pagando com PLS: o router NÃO
// usa a liquidez da própria curva (não existe ainda) -- ele
// faz PLS -> quote via um pool EXTERNO já existente no DEX
// (ex.: o par WPLS/DAI real da PulseX) e só então entrega o
// quote resultante pro buyOnCurve. Isso só funciona se esse
// pool externo já tiver liquidez suficiente; caso contrário
// o swap reverte no próprio DEX, não no launchpad. `minOut`
// aqui protege apenas a etapa final (buyOnCurve); a etapa do
// swap PLS->quote não tem proteção de slippage própria.
if (msg.value == 0) revert SwapRouter_BadValue();
IDexRouter02 curveDex = factory.dexRouter();
address[] memory path = new address[](2);
path[0] = curveDex.WPLS();
path[1] = quote;
uint256 quoteBalBefore = IERC20(quote).balanceOf(address(this));
curveDex.swapExactETHForTokens{value: msg.value}(0, path, address(this), deadline);
uint256 quoteReceived = IERC20(quote).balanceOf(address(this)) - quoteBalBefore;
IERC20(quote).approve(token, quoteReceived);
Token(payable(token)).buyOnCurve(quoteReceived, minOut, _msgSender(), deadline);
} else {
revert SwapRouter_UnsupportedInput();
}
return;
}
// pós-migração: fluxo DEX de sempre
IDexRouter02 dex = factory.dexRouter();
if (inputToken == quote && quote != NATIVE) {
if (msg.value != 0) revert SwapRouter_BadValue();
IERC20(quote).transferFrom(_msgSender(), address(this), amountIn);
IERC20(quote).approve(address(dex), amountIn);
address[] memory path = new address[](2);
path[0] = quote;
path[1] = token;
dex.swapExactTokensForTokensSupportingFeeOnTransferTokens(
amountIn, minOut, path, _msgSender(), deadline
);
} else if (inputToken == NATIVE) {
if (msg.value != amountIn) revert SwapRouter_BadValue();
address[] memory path;
if (quote == NATIVE) {
path = new address[](2);
path[0] = dex.WPLS();
path[1] = token;
} else {
path = new address[](3);
path[0] = dex.WPLS();
path[1] = quote;
path[2] = token;
}
dex.swapExactETHForTokensSupportingFeeOnTransferTokens{value: msg.value}(
minOut, path, _msgSender(), deadline
);
} else {
revert SwapRouter_UnsupportedInput();
}
}
/// @param outputToken NATIVE (recebe PLS) ou o próprio quoteToken do token (recebe o ERC20 quote direto)
function sell(
address token,
uint256 amountIn,
uint256 minOut,
address outputToken,
uint256 deadline
) external nonReentrant {
if (Token(payable(token)).curveActive()) revert SwapRouter_SellDuringCurveUnsupported();
address quote = Token(payable(token)).quoteToken();
IDexRouter02 dex = factory.dexRouter();
IERC20(token).transferFrom(_msgSender(), address(this), amountIn);
IERC20(token).approve(address(dex), amountIn);
if (outputToken == quote && quote != NATIVE) {
address[] memory path = new address[](2);
path[0] = token;
path[1] = quote;
dex.swapExactTokensForTokensSupportingFeeOnTransferTokens(
amountIn, minOut, path, _msgSender(), deadline
);
} else if (outputToken == NATIVE) {
address[] memory path;
if (quote == NATIVE) {
path = new address[](2);
path[0] = token;
path[1] = dex.WPLS();
} else {
path = new address[](3);
path[0] = token;
path[1] = quote;
path[2] = dex.WPLS();
}
dex.swapExactTokensForETHSupportingFeeOnTransferTokens(
amountIn, minOut, path, _msgSender(), deadline
);
} else {
revert SwapRouter_UnsupportedOutput();
}
}
function _msgSender() internal view returns (address) {
return msg.sender;
}
}