Buyback-and-Burn System Development
Buyback-and-burn is deflationary mechanism: protocol uses part of revenue to buy its own token on market and destroy it. BNB, MKR, GMX — all use variations of this mechanism. Implementation requires clear on-chain logic, proper DEX router choice and protection against sandwich attacks.
How The Mechanism Works
Basic scheme: protocol fee -> buyback contract -> swap on DEX -> burn. Key decisions:
Funds source: protocol commissions (trading fees, lending fees, mint fees). Usually contract accumulates USDC/ETH, buyback triggers by schedule or when threshold reached.
DEX for swap: Uniswap V3 via Universal Router or Swap Router 02 — most liquid option for Ethereum/L2. On BSC — PancakeSwap. For large buybacks, routing through multiple pools is important.
Burn mechanism: token.transfer(address(0xdead)) or token.burn(amount) if function exists. Sending to 0xdead — not true burn in terms of totalSupply(), but accepted standard. True burn via _burn() decreases totalSupply.
Buyback Contract
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/access/Ownable2Step.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
interface IBurnable is IERC20 {
function burn(uint256 amount) external;
}
contract BuybackAndBurn is Ownable2Step, ReentrancyGuard {
using SafeERC20 for IERC20;
ISwapRouter public immutable swapRouter;
IERC20 public immutable paymentToken; // USDC/ETH/WETH
IBurnable public immutable projectToken;
address public immutable BURN_ADDRESS = address(0xdead);
uint24 public poolFee = 3000; // 0.3% pool, configurable
uint256 public maxSlippageBps = 200; // 2% maximum slippage
uint256 public minBuybackAmount; // minimum trigger threshold
event BuybackExecuted(
uint256 paymentAmount,
uint256 tokensBought,
uint256 tokensBurned
);
constructor(
address _router,
address _paymentToken,
address _projectToken,
uint256 _minBuybackAmount
) Ownable(msg.sender) {
swapRouter = ISwapRouter(_router);
paymentToken = IERC20(_paymentToken);
projectToken = IBurnable(_projectToken);
minBuybackAmount = _minBuybackAmount;
}
function executeBuyback(uint256 amountIn, uint256 amountOutMinimum)
external
nonReentrant
onlyOwner
{
require(amountIn >= minBuybackAmount, "Below minimum buyback amount");
require(
paymentToken.balanceOf(address(this)) >= amountIn,
"Insufficient balance"
);
paymentToken.approve(address(swapRouter), amountIn);
ISwapRouter.ExactInputSingleParams memory params = ISwapRouter.ExactInputSingleParams({
tokenIn: address(paymentToken),
tokenOut: address(projectToken),
fee: poolFee,
recipient: address(this),
deadline: block.timestamp + 300, // 5 minutes
amountIn: amountIn,
amountOutMinimum: amountOutMinimum, // protection from sandwich
sqrtPriceLimitX96: 0
});
uint256 amountOut = swapRouter.exactInputSingle(params);
// Burn purchased tokens
projectToken.burn(amountOut);
emit BuybackExecuted(amountIn, amountOut, amountOut);
}
// Calculate minAmountOut off-chain via Uniswap SDK before calling
function getMinAmountOut(uint256 amountIn)
external
view
returns (uint256)
{
// This is view-helper for front-end, real calculation via quoter
// quoter.quoteExactInputSingle() outside contract
revert("Use Quoter contract off-chain");
}
}
Protection Against Manipulation
Sandwich attack — main threat to buyback transactions. MEV bots see pending buyback in mempool, insert their swap before it (increasing price) and immediately after (locking in profit). Protection:
- amountOutMinimum: never set to 0. Calculate via Uniswap V3 Quoter with 1-2% buffer.
- Flashbots / MEV blocker: send buyback transactions through private mempool (Flashbots Protect, MEV Blocker from CoW Protocol).
- Splitting into parts: instead of one large buyback — several smaller with interval. Reduces impact and makes predictability lower.
- TWAP pricing: compare swap price with Uniswap V3 oracle TWAP price. If deviation > N% — revert.
// Price check via Uniswap V3 TWAP oracle
function checkPriceDeviation(uint256 amountIn, uint256 amountOut) internal view {
// Get TWAP for last 30 minutes
uint32[] memory secondsAgo = new uint32[](2);
secondsAgo[0] = 1800; // 30 min ago
secondsAgo[1] = 0; // now
(int56[] memory tickCumulatives,) = IUniswapV3Pool(pool)
.observe(secondsAgo);
int56 tickDelta = tickCumulatives[1] - tickCumulatives[0];
int24 twapTick = int24(tickDelta / 1800);
// Calculate expected amountOut by TWAP
uint256 expectedAmountOut = getAmountFromTick(twapTick, amountIn);
// Allow deviation not more than 3% from TWAP
require(
amountOut >= (expectedAmountOut * 97) / 100,
"Price deviation too high"
);
}
Automation and Triggers
Two approaches to triggering buyback:
Keeper-based (recommended): Chainlink Automation or Gelato Network. Smart contract defines condition (checkUpkeep), keeper calls performUpkeep. Decentralized, no trusted server needed.
// Compatibility with Chainlink Automation
function checkUpkeep(bytes calldata)
external
view
returns (bool upkeepNeeded, bytes memory)
{
upkeepNeeded = paymentToken.balanceOf(address(this)) >= minBuybackAmount;
}
function performUpkeep(bytes calldata) external {
require(
paymentToken.balanceOf(address(this)) >= minBuybackAmount,
"Condition not met"
);
uint256 balance = paymentToken.balanceOf(address(this));
uint256 minOut = _calculateMinOut(balance);
executeBuyback(balance, minOut);
}
Schedule-based: daily/weekly buyback by time. Simpler for community communication, but less capital efficient.
Transparency and Reporting
Community should see every buyback. Recommended metrics for public dashboard:
| Metric | Description |
|---|---|
| Total burned | Cumulative burn over all time |
| Buyback frequency | Number of buybacks per period |
| Avg price | Average purchase price |
| Protocol revenue | Revenue directed to buyback |
| Burn rate | % of supply destroyed |
All data comes from on-chain BuybackExecuted events — no additional infrastructure needed, just event indexing.







