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

| Contrat | Adresse |
| --- | --- |
| Factory Pons v2 | `0x7eD598BcEf8bd9Edd8C97A195C6d13f40801EC7e` |
| Escrow des frais | `0xd3AFEB2a57f70eF218Aa82451c51B2fb0416Ac9e` |
| Meme hook (pools v4 après graduation) | `0xE5e702641Ea86F4ae6cC3cDaeD2B886f976Be044` |
| Adresse de burn | `0x000000000000000000000000000000000000dEaD` |

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

| Test | Résultat attendu |
| --- | --- |
| appel depuis un autre wallet que `operator` | revert `NotOperator` |
| `amountIn` > `maxPerCall` | revert `OverCallCap` |
| cumul du jour > `maxPerDay` | revert `OverDayCap`, puis passe le lendemain |
| reçu < `minOut` | revert `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 |
| pause | tout appel revert `IsPaused` |
| fuzz sur `amountIn` et `minOut` | jamais de jeton restant dans le contrat |

## Appels Pons utilisés par Vivra

### Lancement de $VIVRA (une fois)

| Fonction | Contrat | Usage | Statut |
| --- | --- | --- | --- |
| création du token avec `creatorTaxBps = 300`, `creatorFeeRecipient = wallet Collecte`, paire ETH | factory v2 | lancement | fait à la main depuis l'interface Pons ou par script, à vérifier |
| `maxCreatorTaxBps()` | factory v2 | vérifier que 300 est accepté | à lire avant le lancement |
| `getLaunchFeePolicy(token)` | factory v2 | vérifier la répartition de base | à lire après le lancement |

### Job quotidien de trésorerie

| Fonction | Contrat | Usage | Statut |
| --- | --- | --- | --- |
| `pendingFees(poolId)`, `pendingCreatorTax(poolId)` | Meme hook | frais non balayés d'un token gradué | à vérifier |
| `quoteFeeBalance()`, `creatorTaxBalance()` | courbe du token | frais non balayés avant graduation | à vérifier |
| `sweepPoolFees(poolId, minConversionQuoteOut, minBuybackTokensOut)` | Meme hook | balayer vers l'escrow ; erreur `InternalSwapRequiresOperator` = attendre l'opérateur Pons | à vérifier |
| `sweepFees(...)` | courbe du token | idem avant graduation | à vérifier |
| `balanceOf(account)` | escrow | montant réclamable | à vérifier |
| `claim()` ou `claim(uint256 amount)` | escrow | réclamer l'ETH | **divergence connue** : l'ABI publiée dit `claim()`, un retour indique `claim(uint256)` en production |
| `claimToken(token)` | escrow | réclamer un ERC-20 | à vérifier |

### Suivi d'un CTO Pons

| Lecture | Contrat | Usage |
| --- | --- | --- |
| event `CreatorFeeRecipientUpdated` | factory v2 | destinataire des frais changé (CTO actif) |
| `pendingCreatorFeeRecipient(token)` | factory v2 | CTO accepté, en attente (3 jours, puis 3 jours pour exécuter) |
| `transferCreatorFeeRecipient(...)` | factory v2 | côté Pons ou destinataire, selon leur procédure |
| `feeRedirects(token)` | locker v1 | redirection des frais pour un token Pons v1 |
| event `Credited` | escrow | attribution 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.
