# 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)

| Contract | Address |
| --- | --- |
| Pons v2 factory | `0x7eD598BcEf8bd9Edd8C97A195C6d13f40801EC7e` |
| Fee escrow | `0xd3AFEB2a57f70eF218Aa82451c51B2fb0416Ac9e` |
| Meme hook (v4 pools after graduation) | `0xE5e702641Ea86F4ae6cC3cDaeD2B886f976Be044` |
| Burn address | `0x000000000000000000000000000000000000dEaD` |

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

| Test | Expected result |
| --- | --- |
| caller is not `operator` | revert `NotOperator` |
| `amountIn` > `maxPerCall` | revert `OverCallCap` |
| daily total > `maxPerDay` | revert `OverDayCap`, then succeed the next day |
| received < `minOut` | revert `SlippageTooHigh`, no ETH spent |
| normal curve purchase (fork) | contract token balance unchanged, `DEAD` receives everything, event emitted |
| normal v4 pool purchase (fork) | same |
| paused | every purchase reverts `IsPaused` |
| fuzz `amountIn` and `minOut` | no purchased tokens left in the contract |

## Pons calls used by Vivra

### Launching $VIVRA (once)

| Function | Contract | Use | Status |
| --- | --- | --- | --- |
| token creation with `creatorTaxBps = 300`, `creatorFeeRecipient = Collection wallet`, ETH pair | factory v2 | launch | manually through Pons UI or a script; verify |
| `maxCreatorTaxBps()` | factory v2 | check that 300 is accepted | read before launch |
| `getLaunchFeePolicy(token)` | factory v2 | check base allocation | read after launch |

### Daily treasury job

| Function | Contract | Use | Status |
| --- | --- | --- | --- |
| `pendingFees(poolId)`, `pendingCreatorTax(poolId)` | Meme hook | unswept fees for a graduated token | verify |
| `quoteFeeBalance()`, `creatorTaxBalance()` | token curve | unswept fees before graduation | verify |
| `sweepPoolFees(poolId, minConversionQuoteOut, minBuybackTokensOut)` | Meme hook | sweep into escrow; `InternalSwapRequiresOperator` means wait for Pons operator | verify |
| `sweepFees(...)` | token curve | same before graduation | verify |
| `balanceOf(account)` | escrow | claimable amount | verify |
| `claim()` or `claim(uint256 amount)` | escrow | claim ETH | **known discrepancy**: published ABI says `claim()`; a report indicates production uses `claim(uint256)` |
| `claimToken(token)` | escrow | claim ERC-20 | verify |

### Tracking a Pons CTO

| Read | Contract | Use |
| --- | --- | --- |
| `CreatorFeeRecipientUpdated` event | factory v2 | fee recipient changed (CTO active) |
| `pendingCreatorFeeRecipient(token)` | factory v2 | CTO accepted, pending (3-day wait, then 3-day execution window) |
| `transferCreatorFeeRecipient(...)` | factory v2 | called by Pons or recipient according to their procedure |
| `feeRedirects(token)` | locker v1 | fee redirection for a Pons v1 token |
| `Credited` event | escrow | per-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.
