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. Unowner(wallet de l'équipe) peut seulement changer d'operatoret mettre en pause. - Plafonds codés en dur au déploiement :
maxPerCalletmaxPerDay(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#
// 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#
// 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 :
minOutest 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'
operatordans 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, écritledger_entries. - Les clés des hot wallets sont lues depuis le coffre de secrets au démarrage du worker, jamais depuis un fichier.