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");
}
}