cosigo_ (COSIGO)

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

Tags:
ERC20, Token, Mintable, Pausable|addr:0x0f1e8ee8a035270ed9952591d7dbdc600e2b4a49|verified:true|block:23463583|tx:0x3bef21ce3a96a9d492b53e61a6399b6054d1dd8458d2af3206f6bceb48c0fe28|first_check:1759135105

Submitted on: 2025-09-29 10:38:27

Comments

Log in to comment.

No comments yet.