AMM .SOL V3

// SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.19; interface IERC20 { function totalSupply() external view returns (uint256); function balanceOf(address account) external view returns (uint256); function transfer(address to, uint256 amount) external returns (bool); function allowance(address owner, address spender) external view returns (uint256); function approve(address spender, uint256 amount) external returns (bool); function transferFrom(address from, address to, uint256 amount) external returns (bool); event Transfer(address indexed from, address indexed to, uint256 value); event Approval(address indexed owner, address indexed spender, uint256 value); } library SafeERC20 { function safeTransfer(IERC20 token, address to, uint256 value) internal { bytes memory data = abi.encodeWithSelector(token.transfer.selector, to, value); _callOptionalReturn(address(token), data); } function safeTransferFrom(IERC20 token, address from, address to, uint256 value) internal { bytes memory data = abi.encodeWithSelector(token.transferFrom.selector, from, to, value); _callOptionalReturn(address(token), data); } function safeApprove(IERC20 token, address spender, uint256 value) internal { bytes memory data = abi.encodeWithSelector(token.approve.selector, spender, value); _callOptionalReturn(address(token), data); } function _callOptionalReturn(address target, bytes memory data) private { (bool success, bytes memory returndata) = target.call(data); require(success, "SafeERC20: call failed"); if (returndata.length > 0) { require(abi.decode(returndata, (bool)), "SafeERC20: operation unsuccessful"); } } } abstract contract Context { function _msgSender() internal view virtual returns (address) { return msg.sender; } function _msgData() internal view virtual returns (bytes calldata) { return msg.data; } } contract Ownable is Context { address private _owner; event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); constructor() { _transferOwnership(_msgSender()); } function owner() public view virtual returns (address) { return _owner; } modifier onlyOwner() { require(owner() == _msgSender(), "Ownable: caller is not the owner"); _; } function transferOwnership(address newOwner) public virtual onlyOwner { require(newOwner != address(0), "Ownable: new owner is the zero address"); _transferOwnership(newOwner); } function _transferOwnership(address newOwner) internal virtual { address oldOwner = _owner; _owner = newOwner; emit OwnershipTransferred(oldOwner, newOwner); } } contract Pausable is Context { bool private _paused; event Paused(address account); event Unpaused(address account); constructor() { _paused = false; } function paused() public view returns (bool) { return _paused; } modifier whenNotPaused() { require(!_paused, "Pausable: paused"); _; } modifier whenPaused() { require(_paused, "Pausable: not paused"); _; } function _pause() internal virtual whenNotPaused { _paused = true; emit Paused(_msgSender()); } function _unpause() internal virtual whenPaused { _paused = false; emit Unpaused(_msgSender()); } } abstract contract ReentrancyGuard { uint256 private constant _NOT_ENTERED = 1; uint256 private constant _ENTERED = 2; uint256 private _status; constructor() { _status = _NOT_ENTERED; } modifier nonReentrant() { require(_status != _ENTERED, "ReentrancyGuard: reentrant call"); _status = _ENTERED; _; _status = _NOT_ENTERED; } } interface IPresaleController { function setAMM(address amm) external; function setPublicBuyEnabled(bool enabled) external; function setEOAOnly(bool enabled) external; function setGlobalCooldown(uint256 hours_) external; function setAddressCooldownOverride(address user, uint256 hours_, uint256 expiry) external; function setBlacklist(address user, uint256 flags) external; function setBuyQuota(address user, uint256 totalPetro, bool enabled) external; function pause() external; function unpause() external; function isBuyingAllowed(address user) external view returns (bool allowed, uint256 reasonCode); function isSellingAllowed(address user) external view returns (bool allowed, uint256 reasonCode); function getCooldownEnd(address user) external view returns (uint256 ts); function recordUserBuy(address user) external; function canConsumeBuyQuota(address user, uint256 petroAmount) external view returns (bool); function consumeBuyQuota(address user, uint256 petroAmount) external; function getBuyQuota(address user) external view returns (uint256 total, uint256 used, uint256 remaining, uint16 remainingPercentBps); } contract PresaleController is Ownable, Pausable { uint256 public constant FLAG_BAN_BUY = 1; uint256 public constant FLAG_BAN_SELL = 2; uint256 public globalCooldownHours = 3; bool public publicBuyEnabled = false; bool public eoaOnly = false; address public amm; struct CooldownOverride { uint256 hours_; uint256 expiry; } mapping(address => uint256) public blacklistFlags; mapping(address => uint256) public lastBuyDay; mapping(address => uint256) public cooldownUntil; mapping(address => CooldownOverride) public cooldownOverrides; struct Quota { uint256 totalPetro; uint256 usedPetro; bool enabled; } mapping(address => Quota) public buyQuotas; event BuyQuotaUpdated(address indexed user, uint256 totalPetro, bool enabled); event BuyQuotaConsumed(address indexed user, uint256 petroAmount, uint256 usedPetro); event PublicBuyToggled(bool enabled); event EOAOnlyToggled(bool enabled); event GlobalCooldownUpdated(uint256 hours_); event BlacklistUpdated(address indexed user, uint256 flags); event CooldownOverrideUpdated(address indexed user, uint256 hours_, uint256 expiry); function setAMM(address amm_) external onlyOwner { amm = amm_; } function setPublicBuyEnabled(bool enabled) external onlyOwner { publicBuyEnabled = enabled; emit PublicBuyToggled(enabled); } function setEOAOnly(bool enabled) external onlyOwner { eoaOnly = enabled; emit EOAOnlyToggled(enabled); } function setGlobalCooldown(uint256 hours_) external onlyOwner { globalCooldownHours = hours_; emit GlobalCooldownUpdated(hours_); } function setAddressCooldownOverride(address user, uint256 hours_, uint256 expiry) external onlyOwner { cooldownOverrides[user] = CooldownOverride(hours_, expiry); emit CooldownOverrideUpdated(user, hours_, expiry); } function setBlacklist(address user, uint256 flags) external onlyOwner { blacklistFlags[user] = flags; emit BlacklistUpdated(user, flags); } function pause() external onlyOwner { _pause(); } function unpause() external onlyOwner { _unpause(); } function _utc8Day(uint256 ts) internal pure returns (uint256) { return (ts + 8 hours) / 1 days; } function isBuyingAllowed(address user) external view returns (bool allowed, uint256 reasonCode) { if (paused()) return (false, 1); if (!publicBuyEnabled) return (false, 2); if ((blacklistFlags[user] & FLAG_BAN_BUY) != 0) return (false, 3); if (eoaOnly && user.code.length != 0) return (false, 4); uint256 dayNow = _utc8Day(block.timestamp); if (lastBuyDay[user] == dayNow && block.timestamp < cooldownUntil[user]) return (false, 5); return (true, 0); } function isSellingAllowed(address user) external view returns (bool allowed, uint256 reasonCode) { if (paused()) return (false, 1); if ((blacklistFlags[user] & FLAG_BAN_SELL) != 0) return (false, 6); return (true, 0); } function getCooldownEnd(address user) external view returns (uint256 ts) { return cooldownUntil[user]; } function recordUserBuy(address user) external { require(_msgSender() == amm || _msgSender() == owner(), "unauthorized"); uint256 dayNow = _utc8Day(block.timestamp); uint256 hours_ = globalCooldownHours; CooldownOverride memory ov = cooldownOverrides[user]; if (ov.expiry >= block.timestamp && ov.hours_ > 0) { hours_ = ov.hours_; } uint256 midnightUtc8 = (dayNow + 1) * 1 days - 8 hours; uint256 untilTs = block.timestamp + hours_ * 1 hours; if (untilTs > midnightUtc8) { untilTs = midnightUtc8; } lastBuyDay[user] = dayNow; cooldownUntil[user] = untilTs; } function setBuyQuota(address user, uint256 totalPetro, bool enabled) external onlyOwner { Quota storage q = buyQuotas[user]; q.totalPetro = totalPetro; q.enabled = enabled; emit BuyQuotaUpdated(user, totalPetro, enabled); } function getBuyQuota(address user) external view returns (uint256 total, uint256 used, uint256 remaining, uint16 remainingPercentBps) { Quota memory q = buyQuotas[user]; total = q.totalPetro; used = q.usedPetro; remaining = total > used ? total - used : 0; remainingPercentBps = total == 0 ? 0 : uint16((remaining * 10000) / total); } function canConsumeBuyQuota(address user, uint256 petroAmount) external view returns (bool) { Quota memory q = buyQuotas[user]; if (!q.enabled) return true; if (q.totalPetro == 0) return false; return q.usedPetro + petroAmount <= q.totalPetro; } function consumeBuyQuota(address user, uint256 petroAmount) external { require(_msgSender() == amm || _msgSender() == owner(), "unauthorized"); Quota storage q = buyQuotas[user]; if (!q.enabled) return; require(q.usedPetro + petroAmount <= q.totalPetro, "quota exceeded"); q.usedPetro += petroAmount; emit BuyQuotaConsumed(user, petroAmount, q.usedPetro); } } contract PetroAMM is Ownable, Pausable, ReentrancyGuard { using SafeERC20 for IERC20; uint16 public constant BPS_BASE = 10000; IERC20 public immutable GCASH; IERC20 public immutable PETRO; IPresaleController public controller; uint256 private tradingGcash; uint256 private tradingPetro; uint256 private lockedGcash; uint256 private lockedPetro; uint16 public defaultBuyFeeBps = 0; uint16 public defaultSellFeeBps = 30; uint16 public priceImpactBreakerBps = 0; struct FeeOverride { uint16 buyBps; uint16 sellBps; bool enabled; } mapping(address => FeeOverride) public feeOverrides; struct TradeStats { uint256 buysGcash; uint256 buysPetro; uint256 sellsPetro; uint256 sellsGcash; uint256 buyCount; uint256 sellCount; uint256 lastBuyTs; uint256 lastSellTs; } mapping(address => TradeStats) public userStats; struct TradeRecord { bool isBuy; uint256 amountIn; uint256 amountOut; uint16 feeBps; uint256 timestamp; } mapping(address => TradeRecord[10]) private recentTrades; mapping(address => uint8) private recentTradeCount; mapping(address => uint8) private recentTradeIndex; event Swap(address indexed user, address indexed tokenIn, uint256 amountIn, uint256 amountOut, uint16 feeBps); event SwapDetailed( address indexed user, bool isBuy, uint256 amountIn, uint256 amountOut, uint16 feeBps, uint16 impactBps, uint256 gcashResAfter, uint256 petroResAfter, uint256 timestamp ); event ExportCheckpoint( address indexed operator, uint256 fromBlock, uint256 toBlock, string uri, bytes32 contentHash, uint256 tradeCount, uint256 timestamp ); event FeesUpdated(uint16 buyBps, uint16 sellBps); event FeeOverrideUpdated(address indexed user, uint16 buyBps, uint16 sellBps, bool enabled); event LockedReserveChanged(address indexed operator, uint256 newLockedGcash, uint256 newLockedPetro); event TradingReserveChanged(address indexed operator, uint256 newTradingGcash, uint256 newTradingPetro); constructor(address gcash, address petro, address controller_) { require(gcash != address(0) && petro != address(0), "zero addr"); GCASH = IERC20(gcash); PETRO = IERC20(petro); controller = IPresaleController(controller_); } function setController(address controller_) external onlyOwner { controller = IPresaleController(controller_); } function setFees(uint16 buyBps, uint16 sellBps) external onlyOwner { require(buyBps <= BPS_BASE && sellBps <= BPS_BASE, "bps"); defaultBuyFeeBps = buyBps; defaultSellFeeBps = sellBps; emit FeesUpdated(buyBps, sellBps); } function setAddressFeeOverride(address user, uint16 buyBps, uint16 sellBps, bool enabled) external onlyOwner { require(buyBps <= BPS_BASE && sellBps <= BPS_BASE, "bps"); feeOverrides[user] = FeeOverride(buyBps, sellBps, enabled); emit FeeOverrideUpdated(user, buyBps, sellBps, enabled); } function setPriceImpactBreaker(uint16 breakerBps) external onlyOwner { priceImpactBreakerBps = breakerBps; } function pause() external onlyOwner { _pause(); } function unpause() external onlyOwner { _unpause(); } function getTradingReserves() external view returns (uint256 gcashRes, uint256 petroRes) { return (tradingGcash, tradingPetro); } function getLockedReserves() external view returns (uint256 gcashLocked, uint256 petroLocked) { return (lockedGcash, lockedPetro); } function getTotalReserves() external view returns (uint256 totalGcash, uint256 totalPetro) { return (tradingGcash + lockedGcash, tradingPetro + lockedPetro); } function exportCheckpoint(uint256 fromBlock, uint256 toBlock, string calldata uri, bytes32 contentHash, uint256 tradeCount) external onlyOwner { emit ExportCheckpoint(_msgSender(), fromBlock, toBlock, uri, contentHash, tradeCount, block.timestamp); } function hasTraded(address user) external view returns (bool) { TradeStats memory s = userStats[user]; return (s.buyCount + s.sellCount) > 0; } function getUserStats(address user) external view returns ( uint256 buysGcash, uint256 buysPetro, uint256 sellsPetro, uint256 sellsGcash, uint256 buyCount, uint256 sellCount, uint256 lastBuyTs, uint256 lastSellTs ) { TradeStats memory s = userStats[user]; return (s.buysGcash, s.buysPetro, s.sellsPetro, s.sellsGcash, s.buyCount, s.sellCount, s.lastBuyTs, s.lastSellTs); } function getUserAveragePrices(address user) external view returns (uint256 avgBuyPriceE18, uint256 avgSellPriceE18) { TradeStats memory s = userStats[user]; avgBuyPriceE18 = (s.buysPetro == 0) ? 0 : (s.buysGcash * 1e18 / s.buysPetro); avgSellPriceE18 = (s.sellsPetro == 0) ? 0 : (s.sellsGcash * 1e18 / s.sellsPetro); } function getUserRecentTrades(address user) external view returns (TradeRecord[10] memory records, uint8 count) { return (recentTrades[user], recentTradeCount[user]); } function depositTradingReserves(uint256 gcashAmount, uint256 petroAmount) external onlyOwner { if (gcashAmount > 0) { GCASH.safeTransferFrom(_msgSender(), address(this), gcashAmount); tradingGcash += gcashAmount; } if (petroAmount > 0) { PETRO.safeTransferFrom(_msgSender(), address(this), petroAmount); tradingPetro += petroAmount; } emit TradingReserveChanged(_msgSender(), tradingGcash, tradingPetro); } function withdrawTradingReserves(uint256 gcashAmount, uint256 petroAmount, address to) external onlyOwner { require(to != address(0), "to"); if (gcashAmount > 0) { require(tradingGcash >= gcashAmount, "insufficient"); tradingGcash -= gcashAmount; GCASH.safeTransfer(to, gcashAmount); } if (petroAmount > 0) { require(tradingPetro >= petroAmount, "insufficient"); tradingPetro -= petroAmount; PETRO.safeTransfer(to, petroAmount); } emit TradingReserveChanged(_msgSender(), tradingGcash, tradingPetro); } function depositLockedReserves(uint256 gcashAmount, uint256 petroAmount) external onlyOwner { if (gcashAmount > 0) { GCASH.safeTransferFrom(_msgSender(), address(this), gcashAmount); lockedGcash += gcashAmount; } if (petroAmount > 0) { PETRO.safeTransferFrom(_msgSender(), address(this), petroAmount); lockedPetro += petroAmount; } emit LockedReserveChanged(_msgSender(), lockedGcash, lockedPetro); } function withdrawLockedReserves(uint256 gcashAmount, uint256 petroAmount, address to) external onlyOwner { require(to != address(0), "to"); if (gcashAmount > 0) { require(lockedGcash >= gcashAmount, "insufficient"); lockedGcash -= gcashAmount; GCASH.safeTransfer(to, gcashAmount); } if (petroAmount > 0) { require(lockedPetro >= petroAmount, "insufficient"); lockedPetro -= petroAmount; PETRO.safeTransfer(to, petroAmount); } emit LockedReserveChanged(_msgSender(), lockedGcash, lockedPetro); } function _getFeeBps(address user, bool isBuy) internal view returns (uint16) { FeeOverride memory fo = feeOverrides[user]; if (fo.enabled) { return isBuy ? fo.buyBps : fo.sellBps; } return isBuy ? defaultBuyFeeBps : defaultSellFeeBps; } function _quote(uint256 X, uint256 Y, uint256 amountIn) internal pure returns (uint256) { return (amountIn * Y) / (X + amountIn); } function _priceImpactBps(uint256 X, uint256 Y, uint256 amountIn, uint256 amountOutGross) internal pure returns (uint16) { if (X == 0 || Y == 0 || amountOutGross == 0) return 0; uint256 midPriceNum = X; uint256 midPriceDen = Y; uint256 execPriceNum = amountIn; uint256 execPriceDen = amountOutGross; uint256 midScaled = midPriceNum * 1e18 / midPriceDen; uint256 execScaled = execPriceNum * 1e18 / execPriceDen; if (execScaled <= midScaled) return 0; uint256 diff = execScaled - midScaled; uint256 bps = diff * 10000 / midScaled; if (bps > type(uint16).max) return type(uint16).max; return uint16(bps); } function quoteSwap(address tokenIn, uint256 amountIn) external view returns (uint256 amountOut, uint16 feeAppliedBps, uint16 priceImpactBps) { require(amountIn > 0, "amountIn"); bool buy = tokenIn == address(GCASH); require(buy || tokenIn == address(PETRO), "tokenIn"); if (buy) { uint256 gross = _quote(tradingGcash, tradingPetro, amountIn); feeAppliedBps = _getFeeBps(address(0), true); amountOut = gross * (BPS_BASE - feeAppliedBps) / BPS_BASE; priceImpactBps = _priceImpactBps(tradingGcash, tradingPetro, amountIn, gross); } else { uint256 gross = _quote(tradingPetro, tradingGcash, amountIn); feeAppliedBps = _getFeeBps(address(0), false); amountOut = gross * (BPS_BASE - feeAppliedBps) / BPS_BASE; priceImpactBps = _priceImpactBps(tradingPetro, tradingGcash, amountIn, gross); } } function _recordTrade(address user, bool isBuy, uint256 amountIn, uint256 amountOut, uint16 feeBps) internal { TradeStats storage s = userStats[user]; if (isBuy) { s.buysGcash += amountIn; s.buysPetro += amountOut; s.buyCount += 1; s.lastBuyTs = block.timestamp; } else { s.sellsPetro += amountIn; s.sellsGcash += amountOut; s.sellCount += 1; s.lastSellTs = block.timestamp; } uint8 idx = recentTradeIndex[user]; recentTrades[user][idx] = TradeRecord(isBuy, amountIn, amountOut, feeBps, block.timestamp); uint8 c = recentTradeCount[user]; if (c < 10) { recentTradeCount[user] = c + 1; } recentTradeIndex[user] = uint8((uint256(idx) + 1) % 10); } function swap(address tokenIn, uint256 amountIn, uint256 minAmountOut, address to) external nonReentrant whenNotPaused returns (uint256 amountOut) { require(amountIn > 0 && to != address(0), "params"); bool buy = tokenIn == address(GCASH); require(buy || tokenIn == address(PETRO), "tokenIn"); require(tradingGcash > 0 && tradingPetro > 0, "no liquidity"); if (address(controller) != address(0)) { if (buy) { (bool allowed,) = controller.isBuyingAllowed(_msgSender()); require(allowed, "buy not allowed"); } else { (bool allowed,) = controller.isSellingAllowed(_msgSender()); require(allowed, "sell not allowed"); } } if (buy) { uint256 gross = _quote(tradingGcash, tradingPetro, amountIn); uint16 feeBps = _getFeeBps(_msgSender(), true); uint256 net = gross * (BPS_BASE - feeBps) / BPS_BASE; uint16 impactBps = _priceImpactBps(tradingGcash, tradingPetro, amountIn, gross); if (priceImpactBreakerBps > 0) { require(impactBps <= priceImpactBreakerBps, "breaker"); } require(net > 0, "zero-out"); if (address(controller) != address(0)) { require(controller.canConsumeBuyQuota(_msgSender(), net), "quota"); } require(tradingPetro >= net, "insufficient PETRO"); GCASH.safeTransferFrom(_msgSender(), address(this), amountIn); tradingGcash += amountIn; tradingPetro -= net; PETRO.safeTransfer(to, net); amountOut = net; emit Swap(_msgSender(), address(GCASH), amountIn, amountOut, feeBps); emit SwapDetailed(_msgSender(), true, amountIn, amountOut, feeBps, impactBps, tradingGcash, tradingPetro, block.timestamp); if (address(controller) != address(0)) { controller.recordUserBuy(_msgSender()); } if (address(controller) != address(0)) { controller.consumeBuyQuota(_msgSender(), net); } _recordTrade(_msgSender(), true, amountIn, amountOut, feeBps); } else { uint256 gross = _quote(tradingPetro, tradingGcash, amountIn); uint16 feeBps = _getFeeBps(_msgSender(), false); uint256 net = gross * (BPS_BASE - feeBps) / BPS_BASE; uint16 impactBps = _priceImpactBps(tradingPetro, tradingGcash, amountIn, gross); if (priceImpactBreakerBps > 0) { require(impactBps <= priceImpactBreakerBps, "breaker"); } require(net > 0, "zero-out"); require(tradingGcash >= net, "insufficient GCASH"); PETRO.safeTransferFrom(_msgSender(), address(this), amountIn); tradingPetro += amountIn; tradingGcash -= net; GCASH.safeTransfer(to, net); amountOut = net; emit Swap(_msgSender(), address(PETRO), amountIn, amountOut, feeBps); emit SwapDetailed(_msgSender(), false, amountIn, amountOut, feeBps, impactBps, tradingGcash, tradingPetro, block.timestamp); _recordTrade(_msgSender(), false, amountIn, amountOut, feeBps); } require(amountOut >= minAmountOut, "slippage"); } }