Skip to content

Commit 6913b71

Browse files
committed
update FeeSharingCollector to withhold part of fees to the protocol
1 parent b9e4171 commit 6913b71

File tree

6 files changed

+1102
-82
lines changed

6 files changed

+1102
-82
lines changed

contracts/governance/FeeSharingCollector/FeeSharingCollector.sol

Lines changed: 199 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -59,63 +59,6 @@ contract FeeSharingCollector is
5959
address public constant RBTC_DUMMY_ADDRESS_FOR_CHECKPOINT =
6060
address(uint160(uint256(keccak256("RBTC_DUMMY_ADDRESS_FOR_CHECKPOINT"))));
6161

62-
/* Events */
63-
64-
/// @notice Deprecated event after the unification between wrbtc & rbtc
65-
// event FeeWithdrawn(address indexed sender, address indexed token, uint256 amount);
66-
event FeeWithdrawnInRBTC(address indexed sender, uint256 amount);
67-
68-
/// @notice An event emitted when tokens transferred.
69-
event TokensTransferred(address indexed sender, address indexed token, uint256 amount);
70-
71-
/// @notice An event emitted when checkpoint added.
72-
event CheckpointAdded(address indexed sender, address indexed token, uint256 amount);
73-
74-
/// @notice An event emitted when user fee get withdrawn.
75-
event UserFeeWithdrawn(
76-
address indexed sender,
77-
address indexed receiver,
78-
address indexed token,
79-
uint256 amount
80-
);
81-
82-
/// @notice An event emitted when user fee get withdrawn.
83-
event UserFeeProcessedNoWithdraw(
84-
address indexed sender,
85-
address indexed token,
86-
uint256 prevProcessedCheckpoints,
87-
uint256 newProcessedCheckpoints
88-
);
89-
90-
/**
91-
* @notice An event emitted when fee from AMM get withdrawn.
92-
*
93-
* @param sender sender who initiate the withdrawn amm fees.
94-
* @param converter the converter address.
95-
* @param amount total amount of fee (Already converted to WRBTC).
96-
*/
97-
event FeeAMMWithdrawn(address indexed sender, address indexed converter, uint256 amount);
98-
99-
/// @notice An event emitted when converter address has been registered to be whitelisted.
100-
event WhitelistedConverter(address indexed sender, address converter);
101-
102-
/// @notice An event emitted when converter address has been removed from whitelist.
103-
event UnwhitelistedConverter(address indexed sender, address converter);
104-
105-
event RBTCWithdrawn(address indexed sender, address indexed receiver, uint256 amount);
106-
107-
event SetWrbtcToken(
108-
address indexed sender,
109-
address indexed oldWrbtcToken,
110-
address indexed newWrbtcToken
111-
);
112-
113-
event SetLoanTokenWrbtc(
114-
address indexed sender,
115-
address indexed oldLoanTokenWrbtc,
116-
address indexed newLoanTokenWrbtc
117-
);
118-
11962
/* Modifier */
12063
modifier oneTimeExecution(bytes4 _funcSig) {
12164
require(
@@ -207,7 +150,7 @@ contract FeeSharingCollector is
207150
"FeeSharingCollector::withdrawFees: wrbtc token amount exceeds 96 bits"
208151
);
209152

210-
_addCheckpoint(RBTC_DUMMY_ADDRESS_FOR_CHECKPOINT, amount96);
153+
_addCheckpointOrWithholdProtocolFee(RBTC_DUMMY_ADDRESS_FOR_CHECKPOINT, amount96);
211154
}
212155

213156
// note deprecated event since we unify the wrbtc & rbtc
@@ -257,7 +200,10 @@ contract FeeSharingCollector is
257200
}
258201

259202
if (totalPoolTokenAmount > 0) {
260-
_addCheckpoint(RBTC_DUMMY_ADDRESS_FOR_CHECKPOINT, totalPoolTokenAmount);
203+
_addCheckpointOrWithholdProtocolFee(
204+
RBTC_DUMMY_ADDRESS_FOR_CHECKPOINT,
205+
totalPoolTokenAmount
206+
);
261207
}
262208
}
263209

@@ -283,7 +229,7 @@ contract FeeSharingCollector is
283229
_token = RBTC_DUMMY_ADDRESS_FOR_CHECKPOINT;
284230
}
285231

286-
_addCheckpoint(_token, _amount);
232+
_addCheckpointOrWithholdProtocolFee(_token, _amount);
287233

288234
emit TokensTransferred(msg.sender, _token, _amount);
289235
}
@@ -297,35 +243,45 @@ contract FeeSharingCollector is
297243
uint96 _amount = uint96(msg.value);
298244
require(_amount > 0, "FeeSharingCollector::transferRBTC: invalid value");
299245

300-
_addCheckpoint(RBTC_DUMMY_ADDRESS_FOR_CHECKPOINT, _amount);
246+
_addCheckpointOrWithholdProtocolFee(RBTC_DUMMY_ADDRESS_FOR_CHECKPOINT, _amount);
301247

302248
emit TokensTransferred(msg.sender, ZERO_ADDRESS, _amount);
303249
}
304250

305251
/**
306252
* @notice Add checkpoint with accumulated amount by function invocation.
253+
* @dev If token is in protocol withhold list, accumulates fees for protocol (no checkpoint for stakers).
254+
* @dev If token is not in withhold list, creates checkpoint for stakers (100% distribution).
255+
* @dev Does not create zero-value checkpoints to save gas.
307256
* @param _token Address of the token.
257+
* @param _amount Amount of tokens to process.
308258
* */
309-
function _addCheckpoint(address _token, uint96 _amount) internal {
310-
if (block.timestamp - lastFeeWithdrawalTime[_token] >= FEE_WITHDRAWAL_INTERVAL) {
311-
lastFeeWithdrawalTime[_token] = block.timestamp;
312-
uint96 amount = add96(
313-
unprocessedAmount[_token],
314-
_amount,
315-
"FeeSharingCollector::_addCheckpoint: amount exceeds 96 bits"
316-
);
317-
318-
/// @notice Reset unprocessed amount of tokens to zero.
319-
unprocessedAmount[_token] = 0;
320-
321-
/// @notice Write a regular checkpoint.
322-
_writeTokenCheckpoint(_token, amount);
259+
function _addCheckpointOrWithholdProtocolFee(address _token, uint96 _amount) internal {
260+
/// @notice Check if token is in protocol withhold list
261+
if (protocolWithheldTokensList.contains(_token)) {
262+
/// @notice Token is withheld by protocol - directly add to withheld fees (no unprocessedAmount accumulation)
263+
protocolWithheldFees[_token] = protocolWithheldFees[_token].add(_amount);
264+
emit ProtocolRevenueAccumulated(_token, _amount);
323265
} else {
324-
unprocessedAmount[_token] = add96(
325-
unprocessedAmount[_token],
326-
_amount,
327-
"FeeSharingCollector::_addCheckpoint: unprocessedAmount exceeds 96 bits"
328-
);
266+
/// @notice Token is distributed to stakers - use unprocessedAmount and interval logic
267+
if (block.timestamp - lastFeeWithdrawalTime[_token] >= FEE_WITHDRAWAL_INTERVAL) {
268+
lastFeeWithdrawalTime[_token] = block.timestamp;
269+
uint96 amount = add96(
270+
unprocessedAmount[_token],
271+
_amount,
272+
"FeeSharingCollector::_addCheckpointOrWithholdProtocolFee: amount exceeds 96 bits"
273+
);
274+
/// @notice Reset unprocessed amount of tokens to zero.
275+
unprocessedAmount[_token] = 0;
276+
/// @notice Create checkpoint for stakers
277+
_writeTokenCheckpoint(_token, amount);
278+
} else {
279+
unprocessedAmount[_token] = add96(
280+
unprocessedAmount[_token],
281+
_amount,
282+
"FeeSharingCollector::_addCheckpointOrWithholdProtocolFee: unprocessedAmount exceeds 96 bits"
283+
);
284+
}
329285
}
330286
}
331287

@@ -1095,6 +1051,169 @@ contract FeeSharingCollector is
10951051
wrbtcToken.safeTransfer(receiver, wrbtcAmount);
10961052
}
10971053

1054+
/**
1055+
* @notice Add a token to the protocol withhold list.
1056+
* @dev Only callable by owner.
1057+
* @dev Fees from tokens in this list are withheld 100% by protocol (no staker distribution).
1058+
* @dev If there are unprocessed fees accumulated before adding to list, they are moved to protocolWithheldFees.
1059+
* @param token The token address to add.
1060+
*/
1061+
function addProtocolWithholdToken(address token) external onlyOwner {
1062+
require(
1063+
token != ZERO_ADDRESS,
1064+
"FeeSharingCollector::addProtocolWithholdToken: invalid token"
1065+
);
1066+
require(
1067+
!protocolWithheldTokensList.contains(token),
1068+
"FeeSharingCollector::addProtocolWithholdToken: token already in list"
1069+
);
1070+
1071+
/// @notice Move any existing unprocessed amount to protocol withheld fees
1072+
if (unprocessedAmount[token] > 0) {
1073+
protocolWithheldFees[token] = protocolWithheldFees[token].add(
1074+
unprocessedAmount[token]
1075+
);
1076+
emit ProtocolRevenueAccumulated(token, unprocessedAmount[token]);
1077+
unprocessedAmount[token] = 0;
1078+
}
1079+
1080+
protocolWithheldTokensList.add(token);
1081+
emit TokenAddedToProtocolWithholdList(msg.sender, token);
1082+
}
1083+
1084+
/**
1085+
* @notice Remove a token from the protocol withhold list.
1086+
* @dev Only callable by owner.
1087+
* @dev Fees from tokens removed from this list will be distributed to stakers.
1088+
* @param token The token address to remove.
1089+
*/
1090+
function removeProtocolWithholdToken(address token) external onlyOwner {
1091+
require(
1092+
protocolWithheldTokensList.contains(token),
1093+
"FeeSharingCollector::removeProtocolWithholdToken: token not in list"
1094+
);
1095+
1096+
protocolWithheldTokensList.remove(token);
1097+
emit TokenRemovedFromProtocolWithholdList(msg.sender, token);
1098+
}
1099+
1100+
/**
1101+
* @notice Check if a token is in the protocol withhold list.
1102+
* @param token The token address to check.
1103+
* @return True if token fees are withheld by protocol, false otherwise.
1104+
*/
1105+
function isTokenInProtocolWithholdList(address token) external view returns (bool) {
1106+
return protocolWithheldTokensList.contains(token);
1107+
}
1108+
1109+
/**
1110+
* @notice Get all tokens in the protocol withhold list.
1111+
* @return Array of token addresses that are withheld by protocol.
1112+
*/
1113+
function getProtocolWithholdTokensList() external view returns (address[] memory) {
1114+
uint256 length = protocolWithheldTokensList.length();
1115+
address[] memory tokens = new address[](length);
1116+
for (uint256 i = 0; i < length; i++) {
1117+
tokens[i] = protocolWithheldTokensList.get(i);
1118+
}
1119+
return tokens;
1120+
}
1121+
1122+
/**
1123+
* @notice Withdraw all accumulated protocol withheld fees for a token.
1124+
* @dev Only callable by owner. Withdraws the full balance.
1125+
* @param _token The token address to withdraw.
1126+
* Use RBTC_DUMMY_ADDRESS_FOR_CHECKPOINT for RBTC.
1127+
* @param _receiver The address to receive the withheld fees.
1128+
*/
1129+
function withdrawProtocolWithheldFees(
1130+
address _token,
1131+
address _receiver
1132+
) external onlyOwner nonReentrant {
1133+
require(
1134+
_receiver != ZERO_ADDRESS,
1135+
"FeeSharingCollector::withdrawProtocolWithheldFees: invalid receiver"
1136+
);
1137+
1138+
uint256 amount = protocolWithheldFees[_token];
1139+
require(
1140+
amount > 0,
1141+
"FeeSharingCollector::withdrawProtocolWithheldFees: no fees to withdraw"
1142+
);
1143+
1144+
protocolWithheldFees[_token] = 0;
1145+
1146+
// Handle RBTC vs ERC20 token withdrawal
1147+
if (_token == RBTC_DUMMY_ADDRESS_FOR_CHECKPOINT) {
1148+
// Transfer RBTC
1149+
(bool success, ) = _receiver.call.value(amount)("");
1150+
require(
1151+
success,
1152+
"FeeSharingCollector::withdrawProtocolWithheldFees: RBTC transfer failed"
1153+
);
1154+
} else {
1155+
// Transfer ERC20 token
1156+
IERC20(_token).safeTransfer(_receiver, amount);
1157+
}
1158+
1159+
emit ProtocolWithheldFeesWithdrawn(msg.sender, _receiver, _token, amount);
1160+
}
1161+
1162+
/**
1163+
* @notice Withdraw all accumulated protocol withheld fees for multiple tokens in batch.
1164+
* @dev Only callable by owner. Withdraws full balance for each token to the same receiver.
1165+
* @param _tokens Array of token addresses to withdraw.
1166+
* @param _receiver The address to receive all withheld fees.
1167+
*/
1168+
function withdrawProtocolWithheldFeesBatch(
1169+
address[] calldata _tokens,
1170+
address _receiver
1171+
) external onlyOwner nonReentrant {
1172+
require(
1173+
_receiver != ZERO_ADDRESS,
1174+
"FeeSharingCollector::withdrawProtocolWithheldFeesBatch: invalid receiver"
1175+
);
1176+
require(
1177+
_tokens.length > 0,
1178+
"FeeSharingCollector::withdrawProtocolWithheldFeesBatch: empty array"
1179+
);
1180+
1181+
for (uint256 i = 0; i < _tokens.length; i++) {
1182+
address token = _tokens[i];
1183+
uint256 amount = protocolWithheldFees[token];
1184+
1185+
if (amount == 0) {
1186+
continue; // Skip tokens with no fees
1187+
}
1188+
1189+
protocolWithheldFees[token] = 0;
1190+
1191+
// Handle RBTC vs ERC20 token withdrawal
1192+
if (token == RBTC_DUMMY_ADDRESS_FOR_CHECKPOINT) {
1193+
// Transfer RBTC
1194+
(bool success, ) = _receiver.call.value(amount)("");
1195+
require(
1196+
success,
1197+
"FeeSharingCollector::withdrawProtocolWithheldFeesBatch: RBTC transfer failed"
1198+
);
1199+
} else {
1200+
// Transfer ERC20 token
1201+
IERC20(token).safeTransfer(_receiver, amount);
1202+
}
1203+
1204+
emit ProtocolWithheldFeesWithdrawn(msg.sender, _receiver, token, amount);
1205+
}
1206+
}
1207+
1208+
/**
1209+
* @notice Get the accumulated protocol withheld fees for a specific token.
1210+
* @param token The token address to query.
1211+
* @return The accumulated withheld fees amount.
1212+
*/
1213+
function getProtocolWithheldFees(address token) external view returns (uint256) {
1214+
return protocolWithheldFees[token];
1215+
}
1216+
10981217
/**
10991218
* @dev This function is dedicated to recover the wrong fee allocation for the 4 year vesting contracts.
11001219
* This function can only be called once

contracts/governance/FeeSharingCollector/FeeSharingCollectorStorage.sol

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,18 @@ contract FeeSharingCollectorStorage is Ownable {
8686
*/
8787
address public loanTokenWrbtcAddress;
8888

89+
/**
90+
* @dev Enumerable set of tokens for which protocol withholds 100% of fees
91+
* @notice These tokens will NOT create checkpoints for stakers
92+
*/
93+
EnumerableAddressSet.AddressSet internal protocolWithheldTokensList;
94+
95+
/**
96+
* @dev Accumulated protocol fees per token (for tokens in protocolWithheldTokensList)
97+
* @notice token => accumulated amount
98+
*/
99+
mapping(address => uint256) public protocolWithheldFees;
100+
89101
/**
90102
* @dev Prevents a contract from calling itself, directly or indirectly.
91103
* If you mark a function `nonReentrant`, you should also

0 commit comments

Comments
 (0)