vivraDocumentationV1Comprendre le vote

Annexe D · Contrats et appels on-chain

Le seul contrat écrit par Vivra en V1 est le BuybackBurner. Tout le reste est un appel à des contrats Pons existants. Cette page donne l'interface Solidity, un squelette d'implémentation, et la liste des fonctions Pons à appeler.

Les signatures Pons ci-dessous sont celles de la doc Pons v2 publique. Deux points sont déjà signalés comme différents en production (le claim, voir plus bas). Chaque signature doit être vérifiée sur le testnet (chain id 46630) avec l'ABI réellement déployée avant d'écrire le code de production.

Adresses (mainnet, chain id 4663)#

ContratAdresse
Factory Pons v20x7eD598BcEf8bd9Edd8C97A195C6d13f40801EC7e
Escrow des frais0xd3AFEB2a57f70eF218Aa82451c51B2fb0416Ac9e
Meme hook (pools v4 après graduation)0xE5e702641Ea86F4ae6cC3cDaeD2B886f976Be044
Adresse de burn0x000000000000000000000000000000000000dEaD

Les adresses testnet sont à relever dans la doc Pons et à mettre dans la config, jamais en dur dans le code TypeScript.

BuybackBurner#

Règles#

  • Immuable : pas de proxy, pas d'upgrade, pas de fonction de retrait de jetons achetés.
  • Un seul appelant autorisé : le hot wallet du Buyback Bot (operator), fixé au déploiement. Un owner (wallet de l'équipe) peut seulement changer d'operator et mettre en pause.
  • Plafonds codés en dur au déploiement : maxPerCall et maxPerDay (en wei d'ETH).
  • Achat et burn dans la même transaction. Le contrat ne garde jamais de jeton acheté à la fin d'un appel.

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 Achète `token` avec `amountIn` wei d'ETH par la route donnée, puis brûle tout ce qui est reçu.
    /// @param slice Numéro de tranche, repris dans l'event pour le registre.
    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);
}

Par rapport à la page Le Buyback Bot, slice est ajouté en paramètre pour que l'event BuybackBurn porte le numéro de tranche. Le paiement en USDG est hors V1 : $VIVRA et les tokens CTO sont appairés en ETH.

Squelette d'implémentation#

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

    // À compléter avec l'ABI Pons vérifiée sur testnet :
    // achat sur la courbe de lancement du token (avant graduation).
    function _buyOnCurve(address token, uint256 amountIn, uint256 minOut) internal { /* TODO */ }

    // À compléter : swap ETH -> token sur le pool Uniswap v4 du Meme hook (après graduation),
    // via le router universel ou le PoolManager, avec la taxe du hook prise en compte dans minOut.
    function _buyOnV4Pool(address token, uint256 amountIn, uint256 minOut) internal { /* TODO */ }

    receive() external payable {}
}

Points à traiter dans _buyOnCurve et _buyOnV4Pool :

  • La taxe de trade (base Pons et creator tax du token) est prélevée à l'achat : minOut est calculé côté bot après taxe, à partir d'un devis lu juste avant l'envoi.
  • Si l'achat renvoie de l'ETH non utilisé, le contrat le renvoie à l'operator dans la même transaction.
  • Un token encore sur la courbe passe par Route.Curve ; dès que le suivi du token voit la graduation, le bot passe à Route.V4Pool (la page Le Buyback Bot prévoit d'arrêter le buyback à la graduation : c'est le comportement par défaut, réglable).

Tests Foundry attendus#

TestRésultat attendu
appel depuis un autre wallet que operatorrevert NotOperator
amountIn > maxPerCallrevert OverCallCap
cumul du jour > maxPerDayrevert OverDayCap, puis passe le lendemain
reçu < minOutrevert SlippageTooHigh, aucun ETH dépensé
achat normal sur la courbe (fork)solde du contrat inchangé, DEAD reçoit tout, event émis
achat normal sur le pool v4 (fork)idem
pausetout appel revert IsPaused
fuzz sur amountIn et minOutjamais de jeton restant dans le contrat

Appels Pons utilisés par Vivra#

Lancement de $VIVRA (une fois)#

FonctionContratUsageStatut
création du token avec creatorTaxBps = 300, creatorFeeRecipient = wallet Collecte, paire ETHfactory v2lancementfait à la main depuis l'interface Pons ou par script, à vérifier
maxCreatorTaxBps()factory v2vérifier que 300 est acceptéà lire avant le lancement
getLaunchFeePolicy(token)factory v2vérifier la répartition de baseà lire après le lancement

Job quotidien de trésorerie#

FonctionContratUsageStatut
pendingFees(poolId), pendingCreatorTax(poolId)Meme hookfrais non balayés d'un token graduéà vérifier
quoteFeeBalance(), creatorTaxBalance()courbe du tokenfrais non balayés avant graduationà vérifier
sweepPoolFees(poolId, minConversionQuoteOut, minBuybackTokensOut)Meme hookbalayer vers l'escrow ; erreur InternalSwapRequiresOperator = attendre l'opérateur Ponsà vérifier
sweepFees(...)courbe du tokenidem avant graduationà vérifier
balanceOf(account)escrowmontant réclamableà vérifier
claim() ou claim(uint256 amount)escrowréclamer l'ETHdivergence connue : l'ABI publiée dit claim(), un retour indique claim(uint256) en production
claimToken(token)escrowréclamer un ERC-20à vérifier

Suivi d'un CTO Pons#

LectureContratUsage
event CreatorFeeRecipientUpdatedfactory v2destinataire des frais changé (CTO actif)
pendingCreatorFeeRecipient(token)factory v2CTO accepté, en attente (3 jours, puis 3 jours pour exécuter)
transferCreatorFeeRecipient(...)factory v2côté Pons ou destinataire, selon leur procédure
feeRedirects(token)locker v1redirection des frais pour un token Pons v1
event Creditedescrowattribution des frais par token CTO pour le registre

Côté TypeScript (viem)#

  • Une config par chaîne dans packages/chain/config.ts : chain id, RPC principal et de secours, adresses ci-dessus.
  • Les ABI sont des fichiers JSON dans packages/chain/abi/, copiés depuis l'explorateur (contrats vérifiés), jamais réécrits à la main.
  • Toute écriture passe par une fonction qui : simule (simulateContract), vérifie le plafond, envoie, attend 1 confirmation, écrit ledger_entries.
  • Les clés des hot wallets sont lues depuis le coffre de secrets au démarrage du worker, jamais depuis un fichier.
↑ ↓ pour parcourir · Entrée pour ouvrirVIVRA DOCS