vivraDocumentationV1How voting works

Appendix D · Contracts and on-chain calls

The only contract Vivra writes in V1 is BuybackBurner. Everything else calls existing Pons contracts. This page provides the Solidity interface, implementation skeleton, and Pons function list.

The Pons signatures below come from the public Pons v2 documentation used by the source specification. Differences were already reported for production, including claim below. Verify every signature on testnet (chain ID 46630) against the actually deployed ABI before writing production code.

Addresses (mainnet, chain ID 4663)#

ContractAddress
Pons v2 factory0x7eD598BcEf8bd9Edd8C97A195C6d13f40801EC7e
Fee escrow0xd3AFEB2a57f70eF218Aa82451c51B2fb0416Ac9e
Meme hook (v4 pools after graduation)0xE5e702641Ea86F4ae6cC3cDaeD2B886f976Be044
Burn address0x000000000000000000000000000000000000dEaD

Obtain testnet addresses from Pons documentation and put them in configuration, never hard-coded in TypeScript.

BuybackBurner#

Rules#

  • Immutable: no proxy, no upgrade, no function to withdraw purchased tokens.
  • One authorized caller: the Buyback Bot hot wallet (operator), set at deployment. An owner (team wallet) can only change the operator and pause.
  • Deployment-time hard caps: maxPerCall and maxPerDay, in ETH wei.
  • Purchase and burn in the same transaction. The contract never retains purchased tokens at the end of a call.

Interface#

solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

interface IBuybackBurner {
    enum Route { Curve, V4Pool }

    event BuybackBurn(address indexed token, uint256 amountIn, uint256 amountBurned, uint256 indexed slice);
    event OperatorChanged(address indexed previous, address indexed next);
    event Paused(bool paused);

    error NotOperator();
    error IsPaused();
    error OverCallCap();
    error OverDayCap();
    error SlippageTooHigh(uint256 received, uint256 minOut);

    /// @notice Buys `token` with `amountIn` wei of ETH through the specified route, then burns everything received.
    /// @param slice Slice number, included in the event for the ledger.
    function buyAndBurn(address token, Route route, uint256 amountIn, uint256 minOut, uint256 slice)
        external payable returns (uint256 amountBurned);

    function operator() external view returns (address);
    function maxPerCall() external view returns (uint256);
    function maxPerDay() external view returns (uint256);
    function spentToday() external view returns (uint256);
}

Compared with The Buyback Bot page, slice is added as a parameter so the BuybackBurn event carries the slice number. USDG payments are outside V1: $VIVRA and CTO tokens are paired with ETH.

Implementation skeleton#

solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import {IERC20} from "openzeppelin/token/ERC20/IERC20.sol";
import {SafeERC20} from "openzeppelin/token/ERC20/utils/SafeERC20.sol";
import {ReentrancyGuard} from "openzeppelin/utils/ReentrancyGuard.sol";
import {IBuybackBurner} from "./IBuybackBurner.sol";

contract BuybackBurner is IBuybackBurner, ReentrancyGuard {
    using SafeERC20 for IERC20;

    address public constant DEAD = 0x000000000000000000000000000000000000dEaD;

    address public immutable owner;
    uint256 public immutable maxPerCall;
    uint256 public immutable maxPerDay;
    address public operator;
    bool public paused;

    uint256 public currentDay;
    uint256 public spentToday;

    constructor(address _owner, address _operator, uint256 _maxPerCall, uint256 _maxPerDay) {
        owner = _owner;
        operator = _operator;
        maxPerCall = _maxPerCall;
        maxPerDay = _maxPerDay;
    }

    function buyAndBurn(address token, Route route, uint256 amountIn, uint256 minOut, uint256 slice)
        external payable nonReentrant returns (uint256 amountBurned)
    {
        if (msg.sender != operator) revert NotOperator();
        if (paused) revert IsPaused();
        if (msg.value != amountIn || amountIn > maxPerCall) revert OverCallCap();

        uint256 day = block.timestamp / 1 days;
        if (day != currentDay) { currentDay = day; spentToday = 0; }
        if (spentToday + amountIn > maxPerDay) revert OverDayCap();
        spentToday += amountIn;

        uint256 before = IERC20(token).balanceOf(address(this));
        if (route == Route.Curve) {
            _buyOnCurve(token, amountIn, minOut);
        } else {
            _buyOnV4Pool(token, amountIn, minOut);
        }
        amountBurned = IERC20(token).balanceOf(address(this)) - before;
        if (amountBurned < minOut) revert SlippageTooHigh(amountBurned, minOut);

        IERC20(token).safeTransfer(DEAD, amountBurned);
        emit BuybackBurn(token, amountIn, amountBurned, slice);
    }

    function setOperator(address next) external {
        require(msg.sender == owner, "owner");
        emit OperatorChanged(operator, next);
        operator = next;
    }

    function setPaused(bool p) external {
        require(msg.sender == owner, "owner");
        paused = p;
        emit Paused(p);
    }

    // Complete using the Pons ABI verified on testnet:
    // purchase on the token launch curve (before graduation).
    function _buyOnCurve(address token, uint256 amountIn, uint256 minOut) internal { /* TODO */ }

    // Complete: swap ETH -> token through the Meme hook Uniswap v4 pool (after graduation),
    // via Universal Router or PoolManager, including the hook tax in minOut.
    function _buyOnV4Pool(address token, uint256 amountIn, uint256 minOut) internal { /* TODO */ }

    receive() external payable {}
}

Points to handle in _buyOnCurve and _buyOnV4Pool:

  • Trading tax (Pons base fee plus token creator tax) is charged on purchase. The bot calculates minOut after tax using a quote read immediately before sending.
  • If a purchase returns unused ETH, the contract returns it to the operator in the same transaction.
  • A token still on the curve uses Route.Curve; after tracking detects graduation, Route.V4Pool is the pool route. The Buyback Bot page specifies stopping at graduation as the configurable default.

Expected Foundry tests#

TestExpected result
caller is not operatorrevert NotOperator
amountIn > maxPerCallrevert OverCallCap
daily total > maxPerDayrevert OverDayCap, then succeed the next day
received < minOutrevert SlippageTooHigh, no ETH spent
normal curve purchase (fork)contract token balance unchanged, DEAD receives everything, event emitted
normal v4 pool purchase (fork)same
pausedevery purchase reverts IsPaused
fuzz amountIn and minOutno purchased tokens left in the contract

Pons calls used by Vivra#

Launching $VIVRA (once)#

FunctionContractUseStatus
token creation with creatorTaxBps = 300, creatorFeeRecipient = Collection wallet, ETH pairfactory v2launchmanually through Pons UI or a script; verify
maxCreatorTaxBps()factory v2check that 300 is acceptedread before launch
getLaunchFeePolicy(token)factory v2check base allocationread after launch

Daily treasury job#

FunctionContractUseStatus
pendingFees(poolId), pendingCreatorTax(poolId)Meme hookunswept fees for a graduated tokenverify
quoteFeeBalance(), creatorTaxBalance()token curveunswept fees before graduationverify
sweepPoolFees(poolId, minConversionQuoteOut, minBuybackTokensOut)Meme hooksweep into escrow; InternalSwapRequiresOperator means wait for Pons operatorverify
sweepFees(...)token curvesame before graduationverify
balanceOf(account)escrowclaimable amountverify
claim() or claim(uint256 amount)escrowclaim ETHknown discrepancy: published ABI says claim(); a report indicates production uses claim(uint256)
claimToken(token)escrowclaim ERC-20verify

Tracking a Pons CTO#

ReadContractUse
CreatorFeeRecipientUpdated eventfactory v2fee recipient changed (CTO active)
pendingCreatorFeeRecipient(token)factory v2CTO accepted, pending (3-day wait, then 3-day execution window)
transferCreatorFeeRecipient(...)factory v2called by Pons or recipient according to their procedure
feeRedirects(token)locker v1fee redirection for a Pons v1 token
Credited eventescrowper-CTO-token fee attribution for the ledger

TypeScript side (viem)#

  • One configuration per chain in packages/chain/config.ts: chain ID, primary and fallback RPC, addresses above.
  • ABIs are JSON files in packages/chain/abi/, copied from verified explorer contracts, never rewritten manually.
  • Every write goes through a function that simulates (simulateContract), checks caps, sends, waits for 1 confirmation, and writes ledger_entries.
  • Hot-wallet keys are read from the secrets vault when the worker starts, never from a file.
↑ ↓ to navigate · Enter to openVIVRA DOCS