StockDividendDistributorV2
0x6682d7a0541261db789441df9bcc48c4c023980e
Verification
Verified
v0.8.26+commit.8a97fa7a
Type
Contract
9,286 bytes
ABI entries
58
14 read · 15 write
License
none
Contract information
- Address
- 0x6682d7a0541261db789441df9bcc48c4c023980e
- Chain
- Robinhood Chain (4663)
- Compiler
- v0.8.26+commit.8a97fa7a
- Optimization
- Enabled
- Creator
- 0x3ADfACCef9…3D8190ba06
- Creation tx
- 0x4fe4317d1c…bdf944af9a
Token
Not a token
This contract does not expose ERC-20 metadata.
Read contract (14)
BPS() → uint256
dividendToken() → address
isExcluded(address) → bool
isKeeper(address) → bool
measureToken() → address
owner() → address
pendingOwner() → address
poolKey() → tuple
poolManager() → address
poolSet() → bool
sellBps() → uint256
sellToken() → address
totalConverted() → uint256
totalDistributed() → uint256
Events (11)
BeneficiaryReassignedConvertedDividendExcludedSetHarvestKeeperSetMeasureTokenSetOwnershipTransferStartedOwnershipTransferredPoolConfiguredSellBpsSet
ABI
[
{
"inputs": [
{
"internalType": "address",
"name": "_dividendToken",
"type": "address"
},
{
"internalType": "address",
"name": "_measureToken",
"type": "address"
},
{
"internalType": "address",
"name": "_poolManager",
"type": "address"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"inputs": [],
"name": "BadBps",
"type": "error"
},
{
"inputs": [],
"name": "BadPool",
"type": "error"
},
{
"inputs": [],
"name": "BadTotalWeight",
"type": "error"
},
{
"inputs": [],
"name": "EmptyHolders",
"type": "error"
},
{
"inputs": [],
"name": "ExecFailed",
"type": "error"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "got",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "minOut",
"type": "uint256"
}
],
"name": "InsufficientOutput",
"type": "error"
},
{
"inputs": [],
"name": "NotArmed",
"type": "error"
},
{
"inputs": [],
"name": "NotAuthorized",
"type": "error"
},
{
"inputs": [],
"name": "NotOwner",
"type": "error"
},
{
"inputs": [],
"name": "NotPoolManager",
"type": "error"
},
{
"inputs": [],
"name": "NothingToConvert",
"type": "error"
},
{
"inputs": [],
"name": "NothingToDistribute",
"type": "error"
},
{
"inputs": [],
"name": "PoolNotSet",
"type": "error"
},
{
"inputs": [],
"name": "Reentrancy",
"type": "error"
},
{
"inputs": [],
"name": "TransferFailed",
"type": "error"
},
{
"inputs": [],
"name": "ZeroWeight",
"type": "error"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "source",
"type": "address"
},
{
"indexed": true,
"internalType": "bytes32",
"name": "poolId",
"type": "bytes32"
},
{
"indexed": true,
"internalType": "address",
"name": "newBeneficiary",
"type": "address"
}
],
"name": "BeneficiaryReassigned",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "uint256",
"name": "amountIn",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "amountOut",
"type": "uint256"
}
],
"name": "Converted",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "holder",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "Dividend",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "account",
"type": "address"
},
{
"indexed": false,
"internalType": "bool",
"name": "excluded",
"type": "bool"
}
],
"name": "ExcludedSet",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "uint256",
"name": "pot",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "paid",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "holdersPaid",
"type": "uint256"
}
],
"name": "Harvest",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "keeper",
"type": "address"
},
{
"indexed": false,
"internalType": "bool",
"name": "enabled",
"type": "bool"
}
],
"name": "KeeperSet",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "MeasureTokenSet",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "currentOwner",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "pendingOwner",
"type": "address"
}
],
"name": "OwnershipTransferStarted",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "previousOwner",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "OwnershipTransferred",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "sellToken",
"type": "address"
}
],
"name": "PoolConfigured",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "uint256",
"name": "bps",
"type": "uint256"
}
],
"name": "SellBpsSet",
"type": "event"
},
{
"inputs": [],
"name": "BPS",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "acceptOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "source",
"type": "address"
},
{
"internalType": "bytes32",
"name": "poolId",
"type": "bytes32"
}
],
"name": "collectFees",
"outputs": [
{
"internalType": "uint128",
"name": "fees0",
"type": "uint128"
},
{
"internalType": "uint128",
"name": "fees1",
"type": "uint128"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "source",
"type": "address"
},
{
"internalType": "address",
"name": "asset",
"type": "address"
}
],
"name": "collectFeesByAsset",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "minOut",
"type": "uint256"
}
],
"name": "convert",
"outputs": [
{
"internalType": "uint256",
"name": "spent",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "bought",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "dividendToken",
"outputs": [
{
"internalType": "contract IERC20Sdd",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "target",
"type": "address"
},
{
"internalType": "uint256",
"name": "value",
"type": "uint256"
},
{
"internalType": "bytes",
"name": "data",
"type": "bytes"
}
],
"name": "exec",
"outputs": [
{
"internalType": "bytes",
"name": "",
"type": "bytes"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address[]",
"name": "holders",
"type": "address[]"
},
{
"internalType": "uint256",
"name": "totalWeight",
"type": "uint256"
}
],
"name": "harvest",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "isExcluded",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "isKeeper",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "measureToken",
"outputs": [
{
"internalType": "contract IERC20Sdd",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "owner",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "pendingOwner",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "poolKey",
"outputs": [
{
"components": [
{
"internalType": "Currency",
"name": "currency0",
"type": "address"
},
{
"internalType": "Currency",
"name": "currency1",
"type": "address"
},
{
"internalType": "uint24",
"name": "fee",
"type": "uint24"
},
{
"internalType": "int24",
"name": "tickSpacing",
"type": "int24"
},
{
"internalType": "contract IHooks",
"name": "hooks",
"type": "address"
}
],
"internalType": "struct PoolKey",
"name": "",
"type": "tuple"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "poolManager",
"outputs": [
{
"internalType": "contract IPoolManager",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "poolSet",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "source",
"type": "address"
},
{
"internalType": "bytes32",
"name": "poolId",
"type": "bytes32"
},
{
"internalType": "address",
"name": "newBeneficiary",
"type": "address"
}
],
"name": "reassignBeneficiary",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "rescue",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "sellBps",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "sellToken",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "account",
"type": "address"
},
{
"internalType": "bool",
"name": "excluded",
"type": "bool"
}
],
"name": "setExcluded",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "keeper",
"type": "address"
},
{
"internalType": "bool",
"name": "enabled",
"type": "bool"
}
],
"name": "setKeeper",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "setMeasureToken",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"components": [
{
"internalType": "Currency",
"name": "currency0",
"type": "address"
},
{
"internalType": "Currency",
"name": "currency1",
"type": "address"
},
{
"internalType": "uint24",
"name": "fee",
"type": "uint24"
},
{
"internalType": "int24",
"name": "tickSpacing",
"type": "int24"
},
{
"internalType": "contract IHooks",
"name": "hooks",
"type": "address"
}
],
"internalType": "struct PoolKey",
"name": "key",
"type": "tuple"
}
],
"name": "setPool",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "bps",
"type": "uint256"
}
],
"name": "setSellBps",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "totalConverted",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "totalDistributed",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "transferOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes",
"name": "data",
"type": "bytes"
}
],
"name": "unlockCallback",
"outputs": [
{
"internalType": "bytes",
"name": "",
"type": "bytes"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"stateMutability": "payable",
"type": "receive"
}
]Source code
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
import {Currency} from "@uniswap/v4-core/src/types/Currency.sol";
import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
import {IUnlockCallback} from "@uniswap/v4-core/src/interfaces/callback/IUnlockCallback.sol";
import {SwapParams} from "@uniswap/v4-core/src/types/PoolOperation.sol";
import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol";
interface IERC20Sdd {
function balanceOf(address account) external view returns (uint256);
function transfer(address recipient, uint256 amount) external returns (bool);
}
/// Doppler DopplerHookInitializer-style fee source (FeesManager)
interface IFeesByPool {
function collectFees(bytes32 poolId) external returns (uint128 fees0, uint128 fees1);
function updateBeneficiary(bytes32 poolId, address newBeneficiary) external;
}
/// Doppler RehypeDopplerHook-style fee source
interface IFeesByAsset {
function collectFees(address asset) external;
}
/// @notice Production fee receiver + dividend distributor for Doppler-launched tokens
/// on Robinhood Chain.
///
/// Registered as the pool's fee beneficiary, it pulls its share via `collectFees`
/// (fees arrive in both pool currencies), converts an owner-tunable percentage of
/// the non-dividend leg into the dividend token through the same v4 pool
/// (`convert`), and pays out its entire dividend-token balance pro-rata to a
/// keeper-supplied holder list weighted by LIVE measure-token balances (`harvest`).
/// The keeper supplies only the list; amounts cannot be forged.
///
/// Escape hatches, because the beneficiary role is irrevocably bound to this
/// address once assigned: `reassignBeneficiary` forwards the role to a successor,
/// and owner-gated `exec`/`rescue` cover any fee-source variant or stray asset.
contract StockDividendDistributorV2 is IUnlockCallback {
uint256 public constant BPS = 10_000;
uint160 internal constant MIN_SQRT_PRICE = 4295128739;
uint160 internal constant MAX_SQRT_PRICE = 1461446703485210103287273052203988822378723970342;
IERC20Sdd public immutable dividendToken;
IPoolManager public immutable poolManager;
IERC20Sdd public measureToken;
address public owner;
address public pendingOwner;
mapping(address => bool) public isKeeper;
mapping(address => bool) public isExcluded;
// v4 pool used by convert(); one currency must be the dividend token
PoolKey internal _poolKey;
bool public poolSet;
bool internal _sellIsCurrency0;
/// Fraction of the sell-token balance sold per convert() call, in bps.
uint256 public sellBps = 1_000; // 10% default
uint256 public totalDistributed; // dividend tokens paid out, for UI
uint256 public totalConverted; // dividend tokens bought via convert, for UI
bool internal _unlockArmed;
uint256 private _entered;
event Harvest(uint256 pot, uint256 paid, uint256 holdersPaid);
event Dividend(address indexed holder, uint256 amount);
event Converted(uint256 amountIn, uint256 amountOut);
event PoolConfigured(address indexed sellToken);
event SellBpsSet(uint256 bps);
event KeeperSet(address indexed keeper, bool enabled);
event ExcludedSet(address indexed account, bool excluded);
event MeasureTokenSet(address indexed token);
event BeneficiaryReassigned(address indexed source, bytes32 indexed poolId, address indexed newBeneficiary);
event OwnershipTransferStarted(address indexed currentOwner, address indexed pendingOwner);
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
error NotAuthorized();
error NotOwner();
error Reentrancy();
error EmptyHolders();
error NothingToDistribute();
error ZeroWeight();
error BadTotalWeight();
error TransferFailed();
error PoolNotSet();
error BadPool();
error NothingToConvert();
error InsufficientOutput(uint256 got, uint256 minOut);
error NotPoolManager();
error NotArmed();
error BadBps();
error ExecFailed();
modifier onlyOwner() {
if (msg.sender != owner) revert NotOwner();
_;
}
modifier onlyOperator() {
if (!isKeeper[msg.sender] && msg.sender != owner) revert NotAuthorized();
_;
}
modifier nonReentrant() {
if (_entered != 0) revert Reentrancy();
_entered = 1;
_;
_entered = 0;
}
constructor(address _dividendToken, address _measureToken, address _poolManager) {
require(_dividendToken != address(0), "dividend token is zero address");
require(_measureToken != address(0), "measure token is zero address");
require(_poolManager != address(0), "pool manager is zero address");
dividendToken = IERC20Sdd(_dividendToken);
measureToken = IERC20Sdd(_measureToken);
poolManager = IPoolManager(_poolManager);
owner = msg.sender;
isKeeper[msg.sender] = true;
}
receive() external payable {}
// ---------- fee collection ----------
/// @notice Pulls this contract's beneficiary fee share from a Doppler-style fee
/// source keyed by pool id (e.g. DopplerHookInitializer).
function collectFees(address source, bytes32 poolId)
external
nonReentrant
onlyOperator
returns (uint128 fees0, uint128 fees1)
{
(fees0, fees1) = IFeesByPool(source).collectFees(poolId);
}
/// @notice Same for fee sources keyed by asset address (e.g. RehypeDopplerHook).
function collectFeesByAsset(address source, address asset) external nonReentrant onlyOperator {
IFeesByAsset(source).collectFees(asset);
}
// ---------- convert ----------
/// @notice Sells `sellBps` of the current sell-token balance into the dividend
/// token through the configured v4 pool. `minOut` is the caller's slippage
/// floor (quote off-chain); the staged percentage keeps any single call from
/// dumping the full inventory into a thin pool.
function convert(uint256 minOut) external nonReentrant onlyOperator returns (uint256 spent, uint256 bought) {
if (!poolSet) revert PoolNotSet();
Currency sellCurrency = _sellIsCurrency0 ? _poolKey.currency0 : _poolKey.currency1;
uint256 amountIn = IERC20Sdd(Currency.unwrap(sellCurrency)).balanceOf(address(this)) * sellBps / BPS;
if (amountIn == 0) revert NothingToConvert();
_unlockArmed = true;
bytes memory result = poolManager.unlock(abi.encode(amountIn));
_unlockArmed = false;
(spent, bought) = abi.decode(result, (uint256, uint256));
if (bought < minOut) revert InsufficientOutput(bought, minOut);
totalConverted += bought;
emit Converted(spent, bought);
}
function unlockCallback(bytes calldata data) external returns (bytes memory) {
if (msg.sender != address(poolManager)) revert NotPoolManager();
if (!_unlockArmed) revert NotArmed();
uint256 amountIn = abi.decode(data, (uint256));
bool zeroForOne = _sellIsCurrency0;
BalanceDelta delta = poolManager.swap(
_poolKey,
SwapParams({
zeroForOne: zeroForOne,
amountSpecified: -int256(amountIn),
sqrtPriceLimitX96: zeroForOne ? MIN_SQRT_PRICE + 1 : MAX_SQRT_PRICE - 1
}),
""
);
// negative delta = we owe the pool (sell side), positive = owed to us
(int128 d0, int128 d1) = (delta.amount0(), delta.amount1());
uint256 spent = uint256(uint128(zeroForOne ? -d0 : -d1));
uint256 bought = uint256(uint128(zeroForOne ? d1 : d0));
Currency sellCurrency = zeroForOne ? _poolKey.currency0 : _poolKey.currency1;
Currency buyCurrency = zeroForOne ? _poolKey.currency1 : _poolKey.currency0;
if (spent > 0) {
poolManager.sync(sellCurrency);
if (!IERC20Sdd(Currency.unwrap(sellCurrency)).transfer(address(poolManager), spent)) {
revert TransferFailed();
}
poolManager.settle();
}
if (bought > 0) {
poolManager.take(buyCurrency, address(this), bought);
}
return abi.encode(spent, bought);
}
// ---------- harvest ----------
/// @notice Pays the full dividend-token balance to `holders`, weighted by their
/// live measure-token balances. `totalWeight` = 0 uses the batch's own sum;
/// otherwise it must be at least the batch sum (multi-batch payouts take only
/// their fair fraction of the pot).
function harvest(address[] calldata holders, uint256 totalWeight) external nonReentrant onlyOperator {
if (holders.length == 0) revert EmptyHolders();
uint256 pot = dividendToken.balanceOf(address(this));
if (pot == 0) revert NothingToDistribute();
uint256[] memory weights = new uint256[](holders.length);
uint256 batchWeight;
for (uint256 i = 0; i < holders.length; i++) {
address holder = holders[i];
if (holder == address(0) || holder == address(this) || isExcluded[holder]) continue;
uint256 w = measureToken.balanceOf(holder);
weights[i] = w;
batchWeight += w;
}
if (batchWeight == 0) revert ZeroWeight();
uint256 denominator = totalWeight == 0 ? batchWeight : totalWeight;
if (denominator < batchWeight) revert BadTotalWeight();
uint256 paid;
uint256 holdersPaid;
for (uint256 i = 0; i < holders.length; i++) {
uint256 w = weights[i];
if (w == 0) continue;
uint256 amount = pot * w / denominator;
if (amount == 0) continue;
if (!dividendToken.transfer(holders[i], amount)) revert TransferFailed();
paid += amount;
holdersPaid++;
emit Dividend(holders[i], amount);
}
totalDistributed += paid;
emit Harvest(pot, paid, holdersPaid);
}
// ---------- configuration ----------
/// @notice Configures the v4 pool convert() trades through. One currency must
/// be the dividend token; the other becomes the sell token (ERC-20 only).
function setPool(PoolKey calldata key) external onlyOwner {
address c0 = Currency.unwrap(key.currency0);
address c1 = Currency.unwrap(key.currency1);
address dividend = address(dividendToken);
if (c0 != dividend && c1 != dividend) revert BadPool();
_sellIsCurrency0 = c1 == dividend;
address sell = _sellIsCurrency0 ? c0 : c1;
if (sell == address(0)) revert BadPool(); // native sell side unsupported
_poolKey = key;
poolSet = true;
emit PoolConfigured(sell);
}
function poolKey() external view returns (PoolKey memory) {
return _poolKey;
}
function sellToken() public view returns (address) {
if (!poolSet) return address(0);
return Currency.unwrap(_sellIsCurrency0 ? _poolKey.currency0 : _poolKey.currency1);
}
function setSellBps(uint256 bps) external onlyOwner {
if (bps == 0 || bps > BPS) revert BadBps();
sellBps = bps;
emit SellBpsSet(bps);
}
function setKeeper(address keeper, bool enabled) external onlyOwner {
isKeeper[keeper] = enabled;
emit KeeperSet(keeper, enabled);
}
function setExcluded(address account, bool excluded) external onlyOwner {
isExcluded[account] = excluded;
emit ExcludedSet(account, excluded);
}
function setMeasureToken(address token) external onlyOwner {
require(token != address(0), "measure token is zero address");
measureToken = IERC20Sdd(token);
emit MeasureTokenSet(token);
}
// ---------- escape hatches ----------
/// @notice Forwards this contract's beneficiary role on a Doppler fee source to
/// a successor (the role binds to whoever holds it; without this it is stuck
/// here forever).
function reassignBeneficiary(address source, bytes32 poolId, address newBeneficiary) external onlyOwner {
IFeesByPool(source).updateBeneficiary(poolId, newBeneficiary);
emit BeneficiaryReassigned(source, poolId, newBeneficiary);
}
/// @notice Owner escape hatch for fee-source variants this ABI doesn't cover.
/// The owner can already move every asset via rescue(), so this adds no new
/// trust assumption.
function exec(address target, uint256 value, bytes calldata data) external onlyOwner returns (bytes memory) {
(bool ok, bytes memory ret) = target.call{value: value}(data);
if (!ok) revert ExecFailed();
return ret;
}
/// @notice Pulls any token (including the dividend token) or ETH out.
function rescue(address token, address to, uint256 amount) external onlyOwner {
if (token == address(0)) {
(bool ok,) = to.call{value: amount}("");
if (!ok) revert TransferFailed();
} else {
if (!IERC20Sdd(token).transfer(to, amount)) revert TransferFailed();
}
}
// ---------- ownership (two-step) ----------
function transferOwnership(address newOwner) external onlyOwner {
require(newOwner != address(0), "new owner is zero address");
pendingOwner = newOwner;
emit OwnershipTransferStarted(owner, newOwner);
}
function acceptOwnership() external {
if (msg.sender != pendingOwner) revert NotAuthorized();
emit OwnershipTransferred(owner, msg.sender);
owner = msg.sender;
pendingOwner = address(0);
}
}
Chain explorer1948msChain node80ms