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
claimbelow. 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. Anowner(team wallet) can only change theoperatorand pause. - Deployment-time hard caps:
maxPerCallandmaxPerDay, in ETH wei. - Purchase and burn in the same transaction. The contract never retains purchased tokens at the end of a call.
Interface#
// 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#
// 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
minOutafter tax using a quote read immediately before sending. - If a purchase returns unused ETH, the contract returns it to the
operatorin the same transaction. - A token still on the curve uses
Route.Curve; after tracking detects graduation,Route.V4Poolis 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 writesledger_entries. - Hot-wallet keys are read from the secrets vault when the worker starts, never from a file.