// SPDX-License-Identifier: MIT pragma solidity ^0.8.26; /// TBILLY treasury. Holds SGOV (tokenized 0-3 month US Treasuries, Robinhood Chain) bought with the token's /// creator fees. Anyone can burn TBILLY tokens and receive their pro-rata share of the SGOV, at any time. /// No owner. The keeper can only convert what the contract holds (ETH or USDG) into SGOV; it can never withdraw. interface IERC20Min { function balanceOf(address) external view returns (uint256); function transfer(address, uint256) external returns (bool); function transferFrom(address, address, uint256) external returns (bool); function totalSupply() external view returns (uint256); function approve(address, uint256) external returns (bool); } interface IWETH { function deposit() external payable; } interface ISwapRouter02 { struct ExactInputParams { bytes path; address recipient; uint256 amountIn; uint256 amountOutMinimum; } function exactInput(ExactInputParams calldata params) external payable returns (uint256 amountOut); } contract TBilly { IERC20Min public immutable token; // the TBILLY token (Pons) IERC20Min public immutable sgov; // Robinhood SGOV token IERC20Min public immutable usdg; address public immutable weth; ISwapRouter02 public immutable router; // Uniswap SwapRouter02 on Robinhood Chain address public immutable keeper; address public constant DEAD = 0x000000000000000000000000000000000000dEaD; uint24 public constant FEE = 3000; uint256 public totalBurned; uint256 public totalRedeemedSgov; event Funded(address indexed from, uint256 amount); event Swept(uint256 amountIn, bool fromUsdg, uint256 sgovOut); event Redeemed(address indexed who, uint256 tokensBurned, uint256 sgovOut); constructor(address _token, address _sgov, address _usdg, address _weth, address _router, address _keeper) { require(_token != address(0) && _sgov != address(0) && _keeper != address(0), "zero"); token = IERC20Min(_token); sgov = IERC20Min(_sgov); usdg = IERC20Min(_usdg); weth = _weth; router = ISwapRouter02(_router); keeper = _keeper; } receive() external payable { emit Funded(msg.sender, msg.value); } /// ETH held by the treasury -> WETH -> USDG -> SGOV. Keeper only, with a minimum output to block sandwiches. function sweepEth(uint256 minSgovOut) external returns (uint256 out) { require(msg.sender == keeper, "keeper"); uint256 bal = address(this).balance; require(bal > 0, "nothing"); IWETH(weth).deposit{value: bal}(); IERC20Min(weth).approve(address(router), bal); out = router.exactInput(ISwapRouter02.ExactInputParams({ path: abi.encodePacked(weth, FEE, address(usdg), FEE, address(sgov)), recipient: address(this), amountIn: bal, amountOutMinimum: minSgovOut })); emit Swept(bal, false, out); } /// USDG held by the treasury -> SGOV. Keeper only. function sweepUsdg(uint256 minSgovOut) external returns (uint256 out) { require(msg.sender == keeper, "keeper"); uint256 bal = usdg.balanceOf(address(this)); require(bal > 0, "nothing"); usdg.approve(address(router), bal); out = router.exactInput(ISwapRouter02.ExactInputParams({ path: abi.encodePacked(address(usdg), FEE, address(sgov)), recipient: address(this), amountIn: bal, amountOutMinimum: minSgovOut })); emit Swept(bal, true, out); } /// Tokens that still count: total supply minus what was burned and minus what sits in this contract. function circulating() public view returns (uint256) { return token.totalSupply() - token.balanceOf(DEAD) - token.balanceOf(address(this)); } /// SGOV backing each circulating token, in 1e18 SGOV units per 1e18 tokens. function floorPerToken() external view returns (uint256) { uint256 c = circulating(); if (c == 0) return 0; return sgov.balanceOf(address(this)) * 1e18 / c; } function quoteRedeem(uint256 amount) public view returns (uint256) { uint256 c = circulating(); if (c == 0) return 0; return sgov.balanceOf(address(this)) * amount / c; } /// Burn `amount` tokens (sent to the dead address) and receive the pro-rata SGOV. Open to anyone, any time. function redeem(uint256 amount) external returns (uint256 out) { out = quoteRedeem(amount); require(out > 0, "nothing to redeem"); require(token.transferFrom(msg.sender, DEAD, amount), "burn failed"); require(sgov.transfer(msg.sender, out), "sgov transfer failed"); totalBurned += amount; totalRedeemedSgov += out; emit Redeemed(msg.sender, amount, out); } }