Description:
ERC20 token contract with Mintable, Pausable capabilities. Standard implementation for fungible tokens on Ethereum.
Blockchain: Ethereum
Source Code: View Code On The Blockchain
Solidity Source Code:
// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;
/*
* cosigo_ — ERC20-like token (1 token = 1 mg), silver-backed policy gates
* - Roles: owner (admin), custodian (ops/fee receiver)
* - Fees: transfer maintenance fee (to feeSink if set, else custodian), adjustable redemption fee (BPS)
* - Latch: redemption allowed only if floor(spot,premium) >= minFloorCents
* - Deposits: register grams; maxSupply = grams * 1000 * 1e18; mint <= remaining headroom
* - Guards: pause, emergency stop, blacklist, nonReentrant redemption
* - Daily redemption limiter; redemption registry + fulfillment/cancel events
* - Hot/Cold addresses for ops (optional)
* - Anti-bot (optional, owner-configurable): EOA-only, gas-price ceiling, same-block delay, per-address cooldowns
* - NEW:
* • feeSink (configurable) for routing fees
* • burnSink (configurable) for excluding parked tokens and optional hard-burns
* • effectiveSupply(): totalSupply minus feeSink and burnSink balances
*/
abstract contract NonReentrant {
uint256 private _rl;
error Reentrant();
modifier nonReentrant() {
if (_rl != 0) revert Reentrant();
_rl = 1;
_;
_rl = 0;
}
}
contract cosigo_ is NonReentrant {
/* -------------------------------- ERC20 basics -------------------------------- */
string public constant name = "cosigo_";
string public constant symbol = "COSIGO";
uint8 public constant decimals = 18;
uint256 public totalSupply;
mapping(address => uint256) private _bal;
mapping(address => mapping(address => uint256)) private _allow;
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
/* --------------------------------- Roles & Ops -------------------------------- */
// ERRORS
error NotOwner();
error NotCustodian();
error ContractPaused();
error EmergencyActive();
error MarketClosed();
error NotOwnerOrCustodian();
error ErrBlacklisted();
error ErrAllowance();
error ZeroAddress();
error InsufficientBalance();
error NotPendingOwner();
error NotPendingCustodian();
error FloorBelowLatch();
error DailyLimitExceeded();
error NetOutOfBand();
error CannotBlacklistPrivileged();
error AmountZero();
error SameAddress();
error NoChange();
error NoPendingOwner();
error NoPendingCustodian();
error PendingOwnerBlacklisted(address pending);
error PendingCustodianBlacklisted(address pending);
error AlreadyPaused();
error NotPaused();
error EmergencyAlreadyActive();
error EmergencyNotActive();
error UtcOffsetOutOfRange(int256 given);
error SpotNotSet();
error FeeTooHigh(uint256 given, uint256 max);
error PremiumCannotLower(uint256 oldBps, uint256 newBps);
error ShippingFlatTooHigh(uint256 given, uint256 max);
error ShippingPerGramTooHigh(uint256 given, uint256 max);
error GramsZero();
error DocHashEmpty();
error ExceedsHeadroom(uint256 asked, uint256 available);
error BadRedemptionId(uint256 id);
error RedemptionAlreadyFinalized(uint8 status);
error RescueFailed();
// Anti-bot errors
error EoaOnly();
error GasPriceTooHigh(uint256 given, uint256 max);
error TransferSameBlock();
error TransferTooSoon(uint256 nextAllowed);
error RedemptionTooSoon(uint256 nextAllowed);
// ADDRESS
address public owner;
address public custodian;
address public hotAddress;
address public coldAddress;
address public pendingOwner;
address public pendingCustodian;
// fee sink (treasury)
address public feeSink;
// >>> NEW #1: burn sink <<<
address public burnSink;
// EVENT
event RolesSet(address indexed owner, address indexed custodian);
event OwnershipTransferred(address indexed prev, address indexed next);
event CustodianChanged(address indexed oldCustodian, address indexed newCustodian);
event AddressUpdated(string label, address oldAddr, address newAddr);
event OwnershipPending(address indexed current, address indexed pending);
event CustodianPending(address indexed current, address indexed pending);
event Rescue(address indexed token, uint256 amount, address indexed to);
event MaintenanceFeeUpdated(uint256 oldBps, uint256 newBps);
event FeeSinkUpdated(address indexed oldSink, address indexed newSink);
// >>> NEW #1: event for burn sink updates <<<
event BurnSinkUpdated(address indexed oldSink, address indexed newSink);
// --- early state so modifiers can reference them ---
bool public paused;
bool public emergencyStop;
mapping(address => bool) public blacklisted;
// MODIFIERS
modifier onlyOwner() {
if (msg.sender != owner) revert NotOwner();
_;
}
modifier onlyCustodian() {
if (msg.sender != custodian) revert NotCustodian();
_;
}
modifier onlyCustodianOrOwner() {
if (msg.sender != owner && msg.sender != custodian) revert NotOwnerOrCustodian();
_;
}
modifier whenNotPaused() {
if (paused) revert ContractPaused();
_;
}
modifier onlyWhenNotEmergency() {
if (emergencyStop) revert EmergencyActive();
_;
}
// ALLOWANCE HELPERS
function increaseAllowance(address spender, uint256 added)
external
whenNotPaused
returns (bool)
{
if (blacklisted[msg.sender] || blacklisted[spender]) revert ErrBlacklisted();
uint256 cur = _allow[msg.sender][spender];
uint256 val = cur + added;
_allow[msg.sender][spender] = val;
emit Approval(msg.sender, spender, val);
return true;
}
function decreaseAllowance(address spender, uint256 subtracted)
external
whenNotPaused
returns (bool)
{
if (blacklisted[msg.sender] || blacklisted[spender]) revert ErrBlacklisted();
uint256 cur = _allow[msg.sender][spender];
uint256 val = subtracted >= cur ? 0 : cur - subtracted;
_allow[msg.sender][spender] = val;
emit Approval(msg.sender, spender, val);
return true;
}
/* ------------------------------- Ownership / Custodian ------------------------------- */
function transferOwnership(address next) external onlyOwner {
if (next == address(0)) revert ZeroAddress();
if (blacklisted[next]) revert PendingOwnerBlacklisted(next);
if (next == owner) revert NoChange();
pendingOwner = next;
emit OwnershipPending(owner, next);
}
function acceptOwnership() external {
if (pendingOwner == address(0)) revert NoPendingOwner();
if (msg.sender != pendingOwner) revert NotPendingOwner();
if (blacklisted[msg.sender]) revert PendingOwnerBlacklisted(msg.sender);
address old = owner;
owner = pendingOwner;
pendingOwner = address(0);
emit OwnershipTransferred(old, owner);
}
function updateCustodian(address next) external onlyOwner {
if (next == address(0) || next == address(this)) revert ZeroAddress();
if (next == custodian) revert SameAddress();
if (blacklisted[next]) revert PendingCustodianBlacklisted(next);
pendingCustodian = next;
emit CustodianPending(custodian, next); // mark pending
}
function acceptCustodian() external {
if (pendingCustodian == address(0)) revert NoPendingCustodian();
if (msg.sender != pendingCustodian) revert NotPendingCustodian();
if (blacklisted[msg.sender]) revert PendingCustodianBlacklisted(msg.sender);
address old = custodian;
custodian = pendingCustodian;
pendingCustodian = address(0);
emit CustodianChanged(old, custodian);
}
function setHotAddress(address a) external onlyOwner {
if (a == address(0)) revert ZeroAddress();
emit AddressUpdated("Hot", hotAddress, a);
hotAddress = a;
}
function setColdAddress(address a) external onlyOwner {
if (a == address(0)) revert ZeroAddress();
emit AddressUpdated("Cold", coldAddress, a);
coldAddress = a;
}
function setFeeSink(address s) external onlyOwner {
address old = feeSink;
feeSink = s; // can be zero (falls back to custodian)
emit FeeSinkUpdated(old, s);
}
// >>> NEW #2: set burn sink <<<
function setBurnSink(address s) external onlyOwner {
address old = burnSink;
burnSink = s; // can be zero to clear
emit BurnSinkUpdated(old, s);
}
function setMaintenanceFeeBps(uint256 newBps) external onlyOwner {
if (newBps > 300) revert FeeTooHigh(newBps, 300);
uint256 old = maintenanceFeeBps;
if (newBps == old) revert NoChange();
maintenanceFeeBps = newBps;
emit MaintenanceFeeUpdated(old, newBps);
}
function rescueERC20(address token, uint256 amount, address to) external onlyOwner {
if (token == address(this)) revert SameAddress();
if (to == address(0)) revert ZeroAddress();
if (blacklisted[to]) revert ErrBlacklisted();
(bool ok, bytes memory data) =
token.call(abi.encodeWithSignature("transfer(address,uint256)", to, amount));
if (!(ok && (data.length == 0 || abi.decode(data, (bool))))) {
revert RescueFailed();
}
emit Rescue(token, amount, to);
}
function _isPrivileged(address a) internal view returns (bool) {
return a == owner || a == custodian;
}
/* ------------------------------- Admin protections ------------------------------ */
event Paused(address indexed by);
event Unpaused(address indexed by);
event EmergencyStopActivated();
event EmergencyStopDeactivated();
event BlacklistUpdated(address indexed account, bool status);
// Pause / emergency
function pause() external onlyOwner {
if (paused) revert AlreadyPaused();
paused = true;
emit Paused(msg.sender);
}
function unpause() external onlyOwner {
if (!paused) revert NotPaused();
paused = false;
emit Unpaused(msg.sender);
}
function emergencyStopContract() external onlyOwner {
if (emergencyStop) revert EmergencyAlreadyActive();
emergencyStop = true;
emit EmergencyStopActivated();
}
function resumeContract() external onlyOwner {
if (!emergencyStop) revert EmergencyNotActive();
emergencyStop = false;
emit EmergencyStopDeactivated();
}
function updateBlacklist(address a, bool s) external onlyOwner {
if (a == address(0)) revert ZeroAddress();
// Do not allow blacklisting of current owner/custodian. Un-blacklisting is always allowed.
if (s && _isPrivileged(a)) revert CannotBlacklistPrivileged();
blacklisted[a] = s;
emit BlacklistUpdated(a, s);
}
// ---------- Market-hours gating (NYSE 09:30–16:00 local time, Mon–Fri) ----------
bool public marketHoursEnforced = true;
// UTC offset for New York in MINUTES. Use -300 for EST (winter), -240 for EDT (summer).
int256 public marketUtcOffsetMinutes = -300;
event MarketConfigUpdated(bool enforced, int256 utcOffsetMinutes);
function setMarketHours(bool enforced) external onlyOwner {
marketHoursEnforced = enforced;
emit MarketConfigUpdated(enforced, marketUtcOffsetMinutes);
}
/// @notice Set local offset from UTC in minutes (e.g., -300 EST, -240 EDT).
function setMarketUtcOffsetMinutes(int256 minutesOffset) external onlyOwner {
if (minutesOffset < -14 * 60 || minutesOffset > 14 * 60) {
revert UtcOffsetOutOfRange(minutesOffset);
}
marketUtcOffsetMinutes = minutesOffset;
emit MarketConfigUpdated(marketHoursEnforced, minutesOffset);
}
/// @notice True if now is Mon–Fri and local time in [09:30, 16:00)
function marketOpenNow() public view returns (bool) {
if (!marketHoursEnforced) return true;
// Convert to “local” seconds using configured UTC offset
int256 localTs = int256(block.timestamp) + marketUtcOffsetMinutes * 60;
if (localTs < 0) return false;
uint256 t = uint256(localTs);
// dayOfWeek: 0=Sun,1=Mon,...,6=Sat
uint256 dayOfWeek = ((t / 86400) + 4) % 7;
if (dayOfWeek == 0 || dayOfWeek == 6) return false;
uint256 secondsInDay = t % 86400;
uint256 hour = secondsInDay / 3600;
uint256 minute = (secondsInDay % 3600) / 60;
bool afterOpen = (hour > 9) || (hour == 9 && minute >= 30);
bool beforeClose = (hour < 16);
return afterOpen && beforeClose;
}
modifier onlyDuringMarket() {
if (!marketOpenNow()) revert MarketClosed();
_;
}
/* ------------------------------ Anti-bot (optional) ----------------------------- */
bool public enforceEoaOnly; // if true, block contract callers (tx.origin check)
bool public transferDelayEnabled; // if true, an address can't transfer twice in same block
uint256 public maxTxGasPrice; // wei; 0 disables the check
uint256 public cooldownSecTransfers; // per-address transfer cooldown; 0 disables
uint256 public cooldownSecRedemptions; // per-address redeem cooldown; 0 disables
mapping(address => uint256) private _lastTransferBlock;
mapping(address => uint256) private _nextTransferAt;
mapping(address => uint256) private _nextRedeemAt;
event AntiBotConfigUpdated(
bool enforceEoaOnly,
bool transferDelayEnabled,
uint256 maxTxGasPrice,
uint256 cooldownSecTransfers,
uint256 cooldownSecRedemptions
);
function setAntiBotConfig(
bool _enforceEoaOnly,
bool _transferDelayEnabled,
uint256 _maxTxGasPriceWei,
uint256 _cooldownTransfersSec,
uint256 _cooldownRedemptionsSec
) external onlyOwner {
enforceEoaOnly = _enforceEoaOnly;
transferDelayEnabled = _transferDelayEnabled;
maxTxGasPrice = _maxTxGasPriceWei;
cooldownSecTransfers = _cooldownTransfersSec;
cooldownSecRedemptions = _cooldownRedemptionsSec;
emit AntiBotConfigUpdated(
_enforceEoaOnly,
_transferDelayEnabled,
_maxTxGasPriceWei,
_cooldownTransfersSec,
_cooldownRedemptionsSec
);
}
function _antiBotCheckCommon(address actor) internal view {
if (_isPrivileged(actor)) return; // ops must not dead-end
if (enforceEoaOnly && msg.sender != tx.origin) revert EoaOnly();
if (maxTxGasPrice != 0 && tx.gasprice > maxTxGasPrice) {
revert GasPriceTooHigh(tx.gasprice, maxTxGasPrice);
}
}
function _antiBotCheckTransfer(address from) internal view {
_antiBotCheckCommon(from);
if (transferDelayEnabled && _lastTransferBlock[from] == block.number) {
revert TransferSameBlock();
}
if (cooldownSecTransfers != 0 && block.timestamp < _nextTransferAt[from]) {
revert TransferTooSoon(_nextTransferAt[from]);
}
}
function _antiBotPostTransfer(address from) internal {
if (_isPrivileged(from)) return;
if (transferDelayEnabled) _lastTransferBlock[from] = block.number;
if (cooldownSecTransfers != 0) _nextTransferAt[from] = block.timestamp + cooldownSecTransfers;
}
function _antiBotCheckRedeem(address user) internal view {
_antiBotCheckCommon(user);
if (cooldownSecRedemptions != 0 && block.timestamp < _nextRedeemAt[user]) {
revert RedemptionTooSoon(_nextRedeemAt[user]);
}
}
function _antiBotPostRedeem(address user) internal {
if (_isPrivileged(user)) return;
if (cooldownSecRedemptions != 0) _nextRedeemAt[user] = block.timestamp + cooldownSecRedemptions;
}
/* --------------------------------- Constants ----------------------------------- */
uint256 public constant UNIT = 1e18; // token decimals
uint256 public constant BPS_DENOMINATOR = 10_000; // basis points
uint256 public maintenanceFeeBps = 1000; // adjustable 0–3%
/* ------------------------------- Pricing & Latch -------------------------------- */
// cents per mg token (1 token = 1 mg)
uint256 public premiumBps; // raise-only premium
// precise storage (×100 vs cents/mg)
uint256 public spotMicroCentsPerMg; // e.g., 12 = 0.12¢/mg
uint256 public minFloorMicroCentsPerMg; // precise floor
event SpotUpdated(uint256 oldSpotCents, uint256 newSpotCents);
event PremiumSet(uint256 oldBps, uint256 newBps);
event MinFloorCentsUpdated(uint256 oldMinCents, uint256 newMinCents);
event SpotUpdatedMicro(uint256 oldMicro, uint256 newMicro);
event MinFloorUpdatedMicro(uint256 oldMicro, uint256 newMicro);
function setSpotCentsPerToken(uint256 newSpot) external onlyCustodian {
// cents/mg in → store as micro-cents/mg
uint256 oldMicro = spotMicroCentsPerMg;
uint256 oldCents = oldMicro / 100;
spotMicroCentsPerMg = newSpot * 100;
emit SpotUpdated(oldCents, newSpot);
emit SpotUpdatedMicro(oldMicro, spotMicroCentsPerMg);
}
function setSpotMicroCentsPerMg(uint256 micro) external onlyCustodian {
if (micro == 0) revert AmountZero();
uint256 oldMicro = spotMicroCentsPerMg;
spotMicroCentsPerMg = micro;
emit SpotUpdated(oldMicro / 100, micro / 100);
emit SpotUpdatedMicro(oldMicro, micro);
}
function setMinFloorCents(uint256 newMinCents) external onlyOwner {
uint256 oldMicro = minFloorMicroCentsPerMg;
uint256 oldCents = oldMicro / 100;
minFloorMicroCentsPerMg = newMinCents * 100;
emit MinFloorCentsUpdated(oldCents, newMinCents);
emit MinFloorUpdatedMicro(oldMicro, minFloorMicroCentsPerMg);
}
// Optional precise setter (owner): input is micro-cents per mg directly
function setMinFloorMicroCentsPerMg(uint256 micro) external onlyOwner {
require(micro > 0, "floor zero");
uint256 oldMicro = minFloorMicroCentsPerMg;
minFloorMicroCentsPerMg = micro;
emit MinFloorCentsUpdated(oldMicro / 100, micro / 100);
emit MinFloorUpdatedMicro(oldMicro, minFloorMicroCentsPerMg);
}
function getCurrentFloorCents() public view returns (uint256) {
if (spotMicroCentsPerMg == 0) return 0;
uint256 micro = (spotMicroCentsPerMg * (BPS_DENOMINATOR + premiumBps)) / BPS_DENOMINATOR;
return micro / 100;
}
function spotCentsPerToken() public view returns (uint256) {
return spotMicroCentsPerMg / 100;
}
function minFloorCents() public view returns (uint256) {
return minFloorMicroCentsPerMg / 100;
}
function setPremiumBps(uint256 newBps) external onlyOwner {
if (newBps > 3000) revert FeeTooHigh(newBps, 3000);
if (newBps < premiumBps) revert PremiumCannotLower(premiumBps, newBps);
if (newBps == premiumBps) revert NoChange();
uint256 old = premiumBps;
premiumBps = newBps;
emit PremiumSet(old, newBps);
}
/* ------------------------------ Redemption Fee (dyn) ----------------------------- */
uint256 public redemptionFeeBps = 100; // % on redemption (default 1%)
uint256 public shippingFlatCents = 0; // $ flat per redemption (cents)
uint256 public shippingPerGramCents = 0; // $ per gram (cents)
event RedemptionFeeUpdated(uint256 oldFeeBps, uint256 newFeeBps);
event ShippingFeesUpdated(
uint256 oldFlat,
uint256 oldPerGram,
uint256 newFlat,
uint256 newPerGram
);
function setRedemptionFee(uint256 newBps) external onlyCustodianOrOwner {
if (newBps > 3000) revert FeeTooHigh(newBps, 3000);
uint256 old = redemptionFeeBps;
if (newBps == old) revert NoChange();
redemptionFeeBps = newBps;
emit RedemptionFeeUpdated(old, newBps);
}
function setShippingFees(uint256 flatCents, uint256 perGramCents) external onlyOwner {
if (flatCents > 10_000) revert ShippingFlatTooHigh(flatCents, 10_000);
if (perGramCents > 1_000) revert ShippingPerGramTooHigh(perGramCents, 1_000);
uint256 oldFlat = shippingFlatCents;
uint256 oldPer = shippingPerGramCents;
shippingFlatCents = flatCents;
shippingPerGramCents = perGramCents;
emit ShippingFeesUpdated(oldFlat, oldPer, flatCents, perGramCents);
}
/* ------------------------------ Deposits & Supply Cap ---------------------------- */
struct Deposit { uint256 grams; string docHash; uint256 timestamp; }
Deposit[] public deposits;
uint256 public totalDepositedGrams;
uint256 public maxSupply; // grams * 1000 * UNIT (informational)
event DepositRegistered(uint256 grams, string docHash, uint256 timestamp);
function registerDeposit(uint256 grams, string calldata docHash)
external
onlyCustodian
whenNotPaused
{
if (grams == 0) revert GramsZero();
if (bytes(docHash).length == 0) revert DocHashEmpty();
deposits.push(Deposit({grams: grams, docHash: docHash, timestamp: block.timestamp}));
totalDepositedGrams += grams;
maxSupply = totalDepositedGrams * 1000 * UNIT;
emit DepositRegistered(grams, docHash, block.timestamp);
}
function getDepositsCount() external view returns (uint256) { return deposits.length; }
/* ------------------------------ Redemption band & flow --------------------------- */
// per-request NET (after redemption fee) bounds, in mg
uint256 public minRedemptionNetMg; // e.g., 10000 (10 g)
uint256 public maxRedemptionNetMg; // e.g., 3000000 (3000 g)
uint256 public totalFulfilledNetMg;
event RedemptionBandUpdated(
uint256 oldMinNetMg, uint256 oldMaxNetMg, uint256 newMinNetMg, uint256 newMaxNetMg
);
function setRedemptionBand(uint256 newMinNetMg, uint256 newMaxNetMg) external onlyOwner {
if (newMinNetMg < 1000) revert NetOutOfBand();
if (newMaxNetMg < newMinNetMg) revert NetOutOfBand();
emit RedemptionBandUpdated(minRedemptionNetMg, maxRedemptionNetMg, newMinNetMg, newMaxNetMg);
minRedemptionNetMg = newMinNetMg;
maxRedemptionNetMg = newMaxNetMg;
}
// Redemption lifecycle
enum RedemptionStatus { Pending, Fulfilled, Canceled }
struct Redemption {
address requester;
uint256 amountTokensGross; // burned + fee charged
uint256 amountTokensNet; // burned portion (net of fee)
string shippingRef;
uint256 timestamp;
RedemptionStatus status;
}
Redemption[] public redemptions;
event PhysicalRedemptionRequested(
uint256 indexed id, address indexed user, uint256 amountGross, uint256 amountNet, string shippingRef
);
event PhysicalRedemptionFulfilled(uint256 indexed id, address indexed user, uint256 amountNet);
event PhysicalRedemptionCanceled(uint256 indexed id, address indexed user);
function getRedemptionsCount() external view returns (uint256) { return redemptions.length; }
// daily limiter (gross)
uint256 public dailyRedemptionLimit; // tokens (18d); 0 = off
uint256 public dailyRedemptionUsed;
uint256 public lastRedemptionDay; // day index
event DailyRedemptionLimitUpdated(uint256 oldLimit, uint256 newLimit);
function setDailyRedemptionLimit(uint256 newLimit) external onlyOwner {
uint256 old = dailyRedemptionLimit;
dailyRedemptionLimit = newLimit;
emit DailyRedemptionLimitUpdated(old, newLimit);
}
function _rollDay() internal {
uint256 d = block.timestamp / 1 days;
if (d != lastRedemptionDay) {
lastRedemptionDay = d;
dailyRedemptionUsed = 0;
}
}
function redeemPhysicalSilver(uint256 amountTokens, string calldata shippingRef)
external
whenNotPaused
onlyWhenNotEmergency
onlyDuringMarket
nonReentrant
{
_antiBotCheckRedeem(msg.sender);
if (blacklisted[msg.sender]) revert ErrBlacklisted();
if (amountTokens == 0) revert AmountZero();
// latch: spot*premium >= min floor
uint256 floorMicro = (spotMicroCentsPerMg * (BPS_DENOMINATOR + premiumBps)) / BPS_DENOMINATOR;
if (floorMicro < minFloorMicroCentsPerMg) revert FloorBelowLatch();
// daily limiter
_rollDay();
if (dailyRedemptionLimit > 0) {
uint256 used = dailyRedemptionUsed + amountTokens;
if (used > dailyRedemptionLimit) revert DailyLimitExceeded();
dailyRedemptionUsed = used;
}
if (_bal[msg.sender] < amountTokens) revert InsufficientBalance();
// compute fees/net
uint256 feeToTreasury;
uint256 net;
uint256 netMg;
{
uint256 feeRedeem = (amountTokens * redemptionFeeBps) / BPS_DENOMINATOR;
uint256 tmpNet = amountTokens - feeRedeem;
uint256 netMgProv = _mgFromTokens(tmpNet);
uint256 shipCents = _shippingCentsForMg(netMgProv);
uint256 shipTokens = _tokensFromCents(shipCents);
net = tmpNet > shipTokens ? tmpNet - shipTokens : 0;
netMg = _mgFromTokens(net);
if (netMg < minRedemptionNetMg || netMg > maxRedemptionNetMg) revert NetOutOfBand();
feeToTreasury = feeRedeem + shipTokens;
}
// route to feeSink if set, else custodian (back-compat)
address feeDst = (feeSink != address(0)) ? feeSink : custodian;
if (blacklisted[feeDst]) revert ErrBlacklisted();
// effects
unchecked {
_bal[msg.sender] -= amountTokens;
_bal[feeDst] += feeToTreasury;
totalSupply -= net; // burn net
}
emit Transfer(msg.sender, feeDst, feeToTreasury);
emit Transfer(msg.sender, address(0), net);
// record request
uint256 id = redemptions.length;
redemptions.push();
Redemption storage r = redemptions[id];
r.requester = msg.sender;
r.amountTokensGross = amountTokens;
r.amountTokensNet = net;
r.shippingRef = shippingRef;
r.timestamp = block.timestamp;
r.status = RedemptionStatus.Pending;
emit PhysicalRedemptionRequested(id, msg.sender, amountTokens, net, shippingRef);
_antiBotPostRedeem(msg.sender);
}
function cancelPhysicalRedemption(uint256 id) external onlyCustodian {
if (id >= redemptions.length) revert BadRedemptionId(id);
Redemption storage r = redemptions[id];
if (r.status != RedemptionStatus.Pending) {
revert RedemptionAlreadyFinalized(uint8(r.status));
}
// If request and cancel happen same UTC day and a limit is active, free today's usage.
if (dailyRedemptionLimit > 0) {
uint256 reqDay = r.timestamp / 1 days;
uint256 curDay = block.timestamp / 1 days;
if (reqDay == curDay && lastRedemptionDay == curDay) {
// saturating subtract
dailyRedemptionUsed = dailyRedemptionUsed >= r.amountTokensGross
? dailyRedemptionUsed - r.amountTokensGross
: 0;
}
}
// Reverse on-chain token effects from request time:
// At request we burned NET and routed FEE to fee sink/treasury.
// Here we re-mint NET to user. (Optional fee clawback is off-chain.)
_bal[r.requester] += r.amountTokensNet;
totalSupply += r.amountTokensNet;
emit Transfer(address(0), r.requester, r.amountTokensNet);
r.status = RedemptionStatus.Canceled; // terminal state
emit PhysicalRedemptionCanceled(id, r.requester);
}
function fulfillPhysicalRedemption(uint256 id) external onlyCustodian {
if (id >= redemptions.length) revert BadRedemptionId(id);
Redemption storage r = redemptions[id];
if (r.status != RedemptionStatus.Pending) revert RedemptionAlreadyFinalized(uint8(r.status));
r.status = RedemptionStatus.Fulfilled;
// track fulfilled mg backing leaving custody
uint256 netMG = r.amountTokensNet / UNIT; // 1 token = 1 mg
totalFulfilledNetMg += netMG;
emit PhysicalRedemptionFulfilled(id, r.requester, r.amountTokensNet);
}
/* ---------------------------- Pricing helpers ---------------------------------- */
function _mgFromTokens(uint256 tokens) internal pure returns (uint256 mg) {
return tokens / UNIT; // 1 token = 1 mg
}
function _tokensFromCents(uint256 cents) internal view returns (uint256 tokens) {
if (spotMicroCentsPerMg == 0) revert SpotNotSet();
uint256 micro = cents * 100; // cents → micro-cents
return (micro * UNIT) / spotMicroCentsPerMg; // mg tokens (1 token = 1 mg = 1e18)
}
function _shippingCentsForMg(uint256 netMg) internal view returns (uint256 cents) {
// ceil to whole gram
uint256 grams = (netMg + 999) / 1000;
return shippingFlatCents + (shippingPerGramCents * grams);
}
/* -------------------------- Backing & mint headroom ---------------------------- */
/// @dev Total backed capacity in tokens (18d), derived from on-chain deposits minus fulfilled redemptions.
function _backedCapacityTokens() internal view returns (uint256) {
uint256 depositedMg = totalDepositedGrams * 1000;
if (depositedMg <= totalFulfilledNetMg) return 0;
return (depositedMg - totalFulfilledNetMg) * UNIT;
}
/// @dev Remaining mintable tokens under backing (saturating).
function remainingMintable() public view returns (uint256) {
uint256 cap = _backedCapacityTokens();
return cap > totalSupply ? cap - totalSupply : 0;
}
/// @dev Backward-compatible alias.
function _mintableHeadroom() public view returns (uint256) {
return remainingMintable();
}
/// ------------------------- NEW #3: Effective Supply -------------------------
/// Excludes balances held in feeSink and burnSink (if configured).
function effectiveSupply() external view returns (uint256) {
uint256 eff = totalSupply;
if (feeSink != address(0)) {
uint256 f = _bal[feeSink];
if (f > 0 && eff >= f) eff -= f;
}
if (burnSink != address(0)) {
uint256 b = _bal[burnSink];
if (b > 0 && eff >= b) eff -= b;
}
return eff;
}
/* --------------------------------- ERC20 API ----------------------------------- */
function balanceOf(address a) external view returns (uint256) { return _bal[a]; }
function allowance(address a, address s) external view returns (uint256) { return _allow[a][s]; }
function approve(address spender, uint256 value) external whenNotPaused returns (bool) {
if (blacklisted[msg.sender] || blacklisted[spender]) revert ErrBlacklisted();
_allow[msg.sender][spender] = value;
emit Approval(msg.sender, spender, value);
return true;
}
function transfer(address to, uint256 value)
external
whenNotPaused
onlyDuringMarket
returns (bool)
{
_xferWithFee(msg.sender, to, value);
return true;
}
function transferFrom(address from, address to, uint256 value)
external
whenNotPaused
onlyDuringMarket
returns (bool)
{
uint256 cur = _allow[from][msg.sender];
if (cur < value) revert ErrAllowance();
unchecked { _allow[from][msg.sender] = cur - value; }
emit Approval(from, msg.sender, _allow[from][msg.sender]);
_xferWithFee(from, to, value);
return true;
}
function _xferWithFee(address from, address to, uint256 amount) internal {
_antiBotCheckTransfer(from);
if (blacklisted[from] || blacklisted[to]) revert ErrBlacklisted();
if (from == address(0) || to == address(0)) revert ZeroAddress();
if (_bal[from] < amount) revert InsufficientBalance();
uint256 fee = (amount * maintenanceFeeBps) / BPS_DENOMINATOR;
uint256 net = amount - fee;
// route fee to feeSink if set, else custodian
address feeDst = (feeSink != address(0)) ? feeSink : custodian;
if (blacklisted[feeDst]) revert ErrBlacklisted();
unchecked {
_bal[from] -= amount;
_bal[to] += net;
if (fee > 0) _bal[feeDst] += fee;
}
emit Transfer(from, to, net);
if (fee > 0) emit Transfer(from, feeDst, fee);
_antiBotPostTransfer(from);
}
function quoteRedeem(uint256 amountTokens)
external
view
returns (uint256 feeRedeem, uint256 shipTokens, uint256 netTokens, uint256 netMg)
{
feeRedeem = (amountTokens * redemptionFeeBps) / BPS_DENOMINATOR;
uint256 tmpNet = amountTokens - feeRedeem;
uint256 netMgProv = _mgFromTokens(tmpNet);
uint256 shipCents = _shippingCentsForMg(netMgProv);
shipTokens = _tokensFromCents(shipCents);
netTokens = tmpNet > shipTokens ? tmpNet - shipTokens : 0;
netMg = _mgFromTokens(netTokens);
}
/* --------------------------------- Minting ------------------------------------- */
event TokensMinted(address indexed to, uint256 amount);
function mint(address to, uint256 amount)
external
onlyCustodian
whenNotPaused
onlyWhenNotEmergency
{
if (to == address(0)) revert ZeroAddress();
if (blacklisted[to]) revert ErrBlacklisted();
uint256 avail = remainingMintable();
if (amount > avail) revert ExceedsHeadroom(amount, avail);
totalSupply += amount;
_bal[to] += amount;
emit Transfer(address(0), to, amount);
emit TokensMinted(to, amount);
}
// >>> NEW #4: hard-burn from burn sink <<<
/// @notice Permanently burns tokens currently parked at `burnSink`.
/// Shrinks `totalSupply` and emits a burn Transfer.
function burnFromSink(uint256 amount)
external
onlyCustodian
whenNotPaused
onlyWhenNotEmergency
{
if (burnSink == address(0)) revert ZeroAddress();
if (_bal[burnSink] < amount) revert InsufficientBalance();
unchecked {
_bal[burnSink] -= amount;
totalSupply -= amount;
}
emit Transfer(burnSink, address(0), amount);
}
/* ---------------------------- Constructor & bootstrap --------------------------- */
constructor(
address _custodian,
uint256 initDepositGrams,
string memory initDepositDoc,
bool preMintFullSupply
) {
require(_custodian != address(0) && _custodian != address(this), "Invalid custodian");
owner = msg.sender;
custodian = _custodian;
emit RolesSet(owner, custodian);
// Default market policy (override later via setters)
spotMicroCentsPerMg = 5 * 100; // precise store (5¢/mg)
premiumBps = 1000; // +10.00%
minFloorMicroCentsPerMg = 10 * 100; // precise store (10¢/mg)
// fees (defaults)
maintenanceFeeBps = 100; // 1.0% on transfer
redemptionFeeBps = 1000; // 10% on redemption
shippingFlatCents = 0;
shippingPerGramCents = 0;
// Redemption band: 10 g .. 3000 g (NET)
minRedemptionNetMg = 10_000;
maxRedemptionNetMg = 3_000_000;
if (initDepositGrams > 0) {
deposits.push(Deposit({
grams: initDepositGrams,
docHash: initDepositDoc,
timestamp: block.timestamp
}));
totalDepositedGrams += initDepositGrams;
maxSupply = totalDepositedGrams * 1000 * UNIT;
emit DepositRegistered(initDepositGrams, initDepositDoc, block.timestamp);
if (preMintFullSupply) {
uint256 amt = initDepositGrams * 1000 * UNIT;
totalSupply += amt;
_bal[custodian] += amt;
emit Transfer(address(0), custodian, amt);
emit TokensMinted(custodian, amt);
}
}
// start paused for safety
paused = true;
emit Paused(msg.sender);
}
}
Submitted on: 2025-09-29 10:38:27
Comments
Log in to comment.
No comments yet.