TaxCollector
0x086c58400b8708ef993f256e12e752dcf0ac918e
Verification
Verified
v0.8.30+commit.73712a01
Type
Contract
3,916 bytes
ABI entries
25
12 read · 2 write
License
none
Contract information
- Address
- 0x086c58400b8708ef993f256e12e752dcf0ac918e
- Chain
- Robinhood Chain (4663)
- Compiler
- v0.8.30+commit.73712a01
- Optimization
- Enabled
- Creator
- 0xCfBd7e12A0…00068507B9
- Creation tx
- 0xfcd9083b8a…dfe66a53b2
Token
Not a token
This contract does not expose ERC-20 metadata.
Read contract (12)
net() → address
oracle() → address
pTeam() → address
pairContract() → address
pendingNet() → uint256
router() → address
teamBps() → uint256
teamWallet() → address
treasury() → address
treasuryBps() → uint256
usdg() → address
wired() → bool
Events (1)
Converted
ABI
[
{
"inputs": [
{
"internalType": "address",
"name": "net_",
"type": "address"
},
{
"internalType": "address",
"name": "usdg_",
"type": "address"
},
{
"internalType": "address",
"name": "router_",
"type": "address"
},
{
"internalType": "address",
"name": "pair_",
"type": "address"
},
{
"internalType": "address",
"name": "oracle_",
"type": "address"
},
{
"internalType": "address",
"name": "treasury_",
"type": "address"
},
{
"internalType": "address",
"name": "teamWallet_",
"type": "address"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"inputs": [],
"name": "AlreadyWired",
"type": "error"
},
{
"inputs": [],
"name": "ClipTooLarge",
"type": "error"
},
{
"inputs": [],
"name": "MulDivOverflow",
"type": "error"
},
{
"inputs": [],
"name": "NotDeployer",
"type": "error"
},
{
"inputs": [],
"name": "NotWired",
"type": "error"
},
{
"inputs": [],
"name": "SlippageExceeded",
"type": "error"
},
{
"inputs": [],
"name": "TransferFailed",
"type": "error"
},
{
"inputs": [],
"name": "ZeroAddress",
"type": "error"
},
{
"inputs": [],
"name": "ZeroAmount",
"type": "error"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "uint256",
"name": "netIn",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "usdgOutRaw",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "teamUsdgRaw",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "treasuryUsdgRaw",
"type": "uint256"
}
],
"name": "Converted",
"type": "event"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "netAmount",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "minUsdgOutRaw",
"type": "uint256"
}
],
"name": "convert",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "net",
"outputs": [
{
"internalType": "contract IERC20",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "oracle",
"outputs": [
{
"internalType": "contract IPairOracle",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "pTeam",
"outputs": [
{
"internalType": "contract IPTEAM",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "pairContract",
"outputs": [
{
"internalType": "contract IUniswapV2Pair",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "pendingNet",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "router",
"outputs": [
{
"internalType": "contract IUniswapV2Router02",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "teamBps",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "teamWallet",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "treasury",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "treasuryBps",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "usdg",
"outputs": [
{
"internalType": "contract IERC20",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "pTeam_",
"type": "address"
}
],
"name": "wire",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "wired",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
}
]Source code
// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity ^0.8.24;
import {ITaxCollector} from "./interfaces/ITaxCollector.sol";
import {IPTEAM} from "./interfaces/IPTEAM.sol";
import {IPairOracle} from "./interfaces/IPairOracle.sol";
import {IERC20, IERC20Metadata} from "./interfaces/external/IERC20.sol";
import {IUniswapV2Pair, IUniswapV2Router02} from "./interfaces/external/IUniswapV2.sol";
import {Constants} from "./Constants.sol";
import {FixedPointMath} from "./libraries/FixedPointMath.sol";
import {Wired} from "./abstract/Wired.sol";
/// @title TaxCollector — trading-tax accrual and conversion (specs/tax.md §4)
/// @notice NET tax accrues here during swaps (collect-then-convert; this
/// contract never re-enters the pool inside a taxed transfer).
/// `convert()` is permissionless (Q12): batch-swaps NET → USDG on the
/// canonical pool, clip-limited to TAX_SWAP_MAX_CLIP_BPS of pool NET
/// reserves and TWAP-bounded by TAX_SWAP_MAX_DEV_BPS, then splits
/// proceeds by the deterministic decay (mechanism §4):
/// teamBps = 400 × (1 − v), treasuryBps = 500 − teamBps, where v is
/// pTEAM's vestedFraction — one clock, no admin setter.
contract TaxCollector is ITaxCollector, Wired {
error ZeroAmount();
error ClipTooLarge();
error SlippageExceeded();
error TransferFailed();
IERC20 public immutable net;
IERC20 public immutable usdg;
IUniswapV2Router02 public immutable router;
IUniswapV2Pair public immutable pairContract;
IPairOracle public immutable oracle;
address public immutable treasury;
address public immutable teamWallet;
bool private immutable _netIsToken0;
uint256 private immutable _usdgWadFactor;
IPTEAM public pTeam;
constructor(
address net_,
address usdg_,
address router_,
address pair_,
address oracle_,
address treasury_,
address teamWallet_
) {
net = IERC20(_nonZero(net_));
usdg = IERC20(_nonZero(usdg_));
router = IUniswapV2Router02(_nonZero(router_));
pairContract = IUniswapV2Pair(_nonZero(pair_));
oracle = IPairOracle(_nonZero(oracle_));
treasury = _nonZero(treasury_);
teamWallet = _nonZero(teamWallet_);
_netIsToken0 = IUniswapV2Pair(pair_).token0() == net_;
_usdgWadFactor = 10 ** (18 - IERC20Metadata(usdg_).decimals());
}
/// @notice One-time wiring (pTEAM ↔ TaxCollector deploy cycle).
function wire(address pTeam_) external wiring {
pTeam = IPTEAM(_nonZero(pTeam_));
}
/// @inheritdoc ITaxCollector
function convert(uint256 netAmount, uint256 minUsdgOutRaw) external {
_checkWired();
if (netAmount == 0) revert ZeroAmount();
// Clip bound: keep swaps small relative to pool depth.
(uint112 r0, uint112 r1,) = pairContract.getReserves();
uint112 netReserve = _netIsToken0 ? r0 : r1;
if (netAmount * Constants.BPS > uint256(netReserve) * Constants.TAX_SWAP_MAX_CLIP_BPS) {
revert ClipTooLarge();
}
// TWAP bound: reverts on stale oracle (D1) — no conversion without a
// live TWAP.
uint256 twap = oracle.twapNetUsdg();
uint256 expectedWad = FixedPointMath.mulDiv(netAmount, twap, Constants.NET_UNIT);
uint256 minAllowedRaw = expectedWad * (Constants.BPS - Constants.TAX_SWAP_MAX_DEV_BPS)
/ Constants.BPS / _usdgWadFactor;
uint256 minOut = minUsdgOutRaw > minAllowedRaw ? minUsdgOutRaw : minAllowedRaw;
uint256 usdgBefore = usdg.balanceOf(address(this));
if (!net.approve(address(router), netAmount)) revert TransferFailed();
address[] memory path = new address[](2);
path[0] = address(net);
path[1] = address(usdg);
router.swapExactTokensForTokensSupportingFeeOnTransferTokens(
netAmount, minOut, path, address(this), block.timestamp
);
uint256 usdgOut = usdg.balanceOf(address(this)) - usdgBefore;
if (usdgOut < minOut) revert SlippageExceeded();
uint256 teamShare = usdgOut * teamBps() / Constants.TAX_TOTAL_BPS;
uint256 treasuryShare = usdgOut - teamShare;
if (teamShare != 0 && !usdg.transfer(teamWallet, teamShare)) revert TransferFailed();
if (!usdg.transfer(treasury, treasuryShare)) revert TransferFailed();
emit Converted(netAmount, usdgOut, teamShare, treasuryShare);
}
/// @inheritdoc ITaxCollector
function teamBps() public view returns (uint256) {
uint256 v = pTeam.vestedFraction(); // WAD, clamps at 1e18 forever
return Constants.TAX_TEAM_START_BPS * (Constants.WAD - v) / Constants.WAD;
}
/// @inheritdoc ITaxCollector
function treasuryBps() external view returns (uint256) {
return Constants.TAX_TOTAL_BPS - teamBps();
}
/// @inheritdoc ITaxCollector
function pendingNet() external view returns (uint256) {
return net.balanceOf(address(this));
}
}
Chain explorer6537msChain node79ms