Skip to content

Commit 867997d

Browse files
authored
fix: prevent setting fee recipient to self (#7154)
1 parent f73c711 commit 867997d

File tree

3 files changed

+145
-28
lines changed

3 files changed

+145
-28
lines changed

solidity/contracts/token/extensions/HypERC4626Collateral.sol

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -82,17 +82,16 @@ contract HypERC4626Collateral is TokenRouter {
8282
) public payable override returns (bytes32 messageId) {
8383
// 1. Calculate the fee amounts, charge the sender and distribute to feeRecipient if necessary
8484
// Don't use HypERC4626Collateral's implementation of _transferTo since it does a redemption.
85-
uint256 feeRecipientFee = _feeRecipientAmount(
85+
(address _feeRecipient, uint256 feeAmount) = _feeRecipientAndAmount(
8686
_destination,
8787
_recipient,
8888
_amount
8989
);
90-
_transferFromSender(_amount + feeRecipientFee);
91-
if (feeRecipientFee > 0) {
92-
wrappedToken._transferTo(feeRecipient(), feeRecipientFee);
90+
_transferFromSender(_amount + feeAmount);
91+
if (feeAmount > 0) {
92+
wrappedToken._transferTo(_feeRecipient, feeAmount);
9393
}
9494

95-
// 2. Prepare the token message with the recipient, amount, and any additional metadata in overrides
9695
// Deposit the amount into the vault and get the shares for the TokenMessage amount
9796
uint256 _shares = _depositIntoVault(_amount);
9897

solidity/contracts/token/libs/TokenRouter.sol

Lines changed: 27 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {TypeCasts} from "../../libs/TypeCasts.sol";
66
import {GasRouter} from "../../client/GasRouter.sol";
77
import {TokenMessage} from "./TokenMessage.sol";
88
import {Quote, ITokenBridge, ITokenFee} from "../../interfaces/ITokenBridge.sol";
9+
import {Quotes} from "./Quotes.sol";
910
import {StorageSlot} from "@openzeppelin/contracts/utils/StorageSlot.sol";
1011

1112
/**
@@ -24,6 +25,7 @@ abstract contract TokenRouter is GasRouter, ITokenBridge {
2425
using TypeCasts for address;
2526
using TokenMessage for bytes;
2627
using StorageSlot for bytes32;
28+
using Quotes for Quote[];
2729

2830
/**
2931
* @dev Emitted on `transferRemote` when a transfer message is dispatched.
@@ -80,7 +82,7 @@ abstract contract TokenRouter is GasRouter, ITokenBridge {
8082
/**
8183
* @inheritdoc ITokenFee
8284
* @notice Implements the standardized fee quoting interface for token transfers based on
83-
* overridable internal functions of _quoteGasPayment, _feeRecipientAmount, and _externalFeeAmount.
85+
* overridable internal functions of _quoteGasPayment, _feeRecipientAndAmount, and _externalFeeAmount.
8486
* @param _destination The identifier of the destination chain.
8587
* @param _recipient The address of the recipient on the destination chain.
8688
* @param _amount The amount or identifier of tokens to be sent to the remote recipient
@@ -103,11 +105,12 @@ abstract contract TokenRouter is GasRouter, ITokenBridge {
103105
token: address(0),
104106
amount: _quoteGasPayment(_destination, _recipient, _amount)
105107
});
106-
quotes[1] = Quote({
107-
token: token(),
108-
amount: _amount +
109-
_feeRecipientAmount(_destination, _recipient, _amount)
110-
});
108+
(, uint256 feeAmount) = _feeRecipientAndAmount(
109+
_destination,
110+
_recipient,
111+
_amount
112+
);
113+
quotes[1] = Quote({token: token(), amount: _amount + feeAmount});
111114
quotes[2] = Quote({
112115
token: token(),
113116
amount: _externalFeeAmount(_destination, _recipient, _amount)
@@ -175,18 +178,18 @@ abstract contract TokenRouter is GasRouter, ITokenBridge {
175178
uint256 _amount,
176179
uint256 _msgValue
177180
) internal returns (uint256 externalFee, uint256 remainingNativeValue) {
178-
uint256 feeRecipientFee = _feeRecipientAmount(
181+
(address _feeRecipient, uint256 feeAmount) = _feeRecipientAndAmount(
179182
_destination,
180183
_recipient,
181184
_amount
182185
);
183186
externalFee = _externalFeeAmount(_destination, _recipient, _amount);
184-
uint256 charge = _amount + feeRecipientFee + externalFee;
187+
uint256 charge = _amount + feeAmount + externalFee;
185188
_transferFromSender(charge);
186-
if (feeRecipientFee > 0) {
189+
if (feeAmount > 0) {
187190
// transfer atomically so we don't need to keep track of collateral
188191
// and fee balances separately
189-
_transferTo(feeRecipient(), feeRecipientFee);
192+
_transferTo(_feeRecipient, feeAmount);
190193
}
191194
remainingNativeValue = token() != address(0)
192195
? _msgValue
@@ -221,11 +224,12 @@ abstract contract TokenRouter is GasRouter, ITokenBridge {
221224
/**
222225
* @notice Sets the fee recipient for the router.
223226
* @dev Allows for address(0) to be set, which disables fees.
224-
* @param _feeRecipient The address of the fee recipient.
227+
* @param recipient The address that receives fees.
225228
*/
226-
function setFeeRecipient(address _feeRecipient) public onlyOwner {
227-
FEE_RECIPIENT_SLOT.getAddressSlot().value = _feeRecipient;
228-
emit FeeRecipientSet(_feeRecipient);
229+
function setFeeRecipient(address recipient) public onlyOwner {
230+
require(recipient != address(this), "Fee recipient cannot be self");
231+
FEE_RECIPIENT_SLOT.getAddressSlot().value = recipient;
232+
emit FeeRecipientSet(recipient);
229233
}
230234

231235
/**
@@ -264,32 +268,34 @@ abstract contract TokenRouter is GasRouter, ITokenBridge {
264268
* @param _destination The identifier of the destination chain.
265269
* @param _recipient The address of the recipient on the destination chain.
266270
* @param _amount The amount or identifier of tokens to be sent to the remote recipient
271+
* @return _feeRecipient The address of the fee recipient.
267272
* @return feeAmount The fee recipient amount.
268273
* @dev This function is is not intended to be overridden as storage and logic is contained in TokenRouter.
269274
*/
270-
function _feeRecipientAmount(
275+
function _feeRecipientAndAmount(
271276
uint32 _destination,
272277
bytes32 _recipient,
273278
uint256 _amount
274-
) internal view returns (uint256 feeAmount) {
275-
if (feeRecipient() == address(0)) {
276-
return 0;
279+
) internal view returns (address _feeRecipient, uint256 feeAmount) {
280+
_feeRecipient = feeRecipient();
281+
if (_feeRecipient == address(0)) {
282+
return (_feeRecipient, 0);
277283
}
278284

279-
Quote[] memory quotes = ITokenFee(feeRecipient()).quoteTransferRemote(
285+
Quote[] memory quotes = ITokenFee(_feeRecipient).quoteTransferRemote(
280286
_destination,
281287
_recipient,
282288
_amount
283289
);
284290
if (quotes.length == 0) {
285-
return 0;
291+
return (_feeRecipient, 0);
286292
}
287293

288294
require(
289295
quotes.length == 1 && quotes[0].token == token(),
290296
"FungibleTokenRouter: fee must match token"
291297
);
292-
return quotes[0].amount;
298+
feeAmount = quotes[0].amount;
293299
}
294300

295301
/**

solidity/test/token/HypnativeMovable.t.sol

Lines changed: 114 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,23 @@ import {LinearFee} from "contracts/token/fees/LinearFee.sol";
1212
import "forge-std/Test.sol";
1313

1414
contract MockITokenBridgeEth is ITokenBridge {
15-
constructor() {}
15+
uint256 public quoteLength;
16+
address public quoteToken;
17+
uint256 public quoteAmount;
18+
19+
constructor() {
20+
quoteLength = 0;
21+
}
22+
23+
function setQuote(
24+
uint256 _length,
25+
address _token,
26+
uint256 _amount
27+
) external {
28+
quoteLength = _length;
29+
quoteToken = _token;
30+
quoteAmount = _amount;
31+
}
1632

1733
function transferRemote(
1834
uint32 destinationDomain,
@@ -27,7 +43,15 @@ contract MockITokenBridgeEth is ITokenBridge {
2743
bytes32 recipient,
2844
uint256 amountOut
2945
) external view override returns (Quote[] memory) {
30-
return new Quote[](0);
46+
Quote[] memory quotes = new Quote[](quoteLength);
47+
if (quoteLength == 1) {
48+
quotes[0] = Quote({token: quoteToken, amount: quoteAmount});
49+
} else if (quoteLength > 1) {
50+
// Return multiple quotes for testing
51+
quotes[0] = Quote({token: quoteToken, amount: quoteAmount});
52+
quotes[1] = Quote({token: address(0), amount: 100});
53+
}
54+
return quotes;
3155
}
3256
}
3357

@@ -113,4 +137,92 @@ contract HypNativeMovableTest is Test {
113137
assertEq(address(router).balance, balance - amount);
114138
assertEq(address(vtb).balance, amount);
115139
}
140+
141+
function test_setFeeRecipient_cannotSetToSelf() public {
142+
vm.expectRevert("Fee recipient cannot be self");
143+
router.setFeeRecipient(address(router));
144+
}
145+
146+
function test_setFeeRecipient_canSetToOtherAddress() public {
147+
address feeRecipient = address(0x123);
148+
router.setFeeRecipient(feeRecipient);
149+
assertEq(router.feeRecipient(), feeRecipient);
150+
}
151+
152+
function test_setFeeRecipient_canSetToZeroAddress() public {
153+
router.setFeeRecipient(address(0x123));
154+
assertEq(router.feeRecipient(), address(0x123));
155+
156+
router.setFeeRecipient(address(0));
157+
assertEq(router.feeRecipient(), address(0));
158+
}
159+
160+
function test_feeRecipient_emptyQuotesReturnsZero() public {
161+
MockITokenBridgeEth mockFeeRecipient = new MockITokenBridgeEth();
162+
// Set to return empty quotes (length 0)
163+
mockFeeRecipient.setQuote(0, address(0), 0);
164+
165+
router.setFeeRecipient(address(mockFeeRecipient));
166+
167+
// Should not revert and return 0 fee
168+
Quote[] memory quotes = router.quoteTransferRemote(
169+
destinationDomain,
170+
bytes32(uint256(uint160(alice))),
171+
1 ether
172+
);
173+
174+
// quotes[1] is the internal fee (amount + fee)
175+
assertEq(quotes[1].amount, 1 ether); // no fee added
176+
}
177+
178+
function test_feeRecipient_multipleQuotesReverts() public {
179+
MockITokenBridgeEth mockFeeRecipient = new MockITokenBridgeEth();
180+
// Set to return 2 quotes (invalid)
181+
mockFeeRecipient.setQuote(2, address(0), 0.1 ether);
182+
183+
router.setFeeRecipient(address(mockFeeRecipient));
184+
185+
// Should revert with the fee mismatch error
186+
vm.expectRevert("FungibleTokenRouter: fee must match token");
187+
router.quoteTransferRemote(
188+
destinationDomain,
189+
bytes32(uint256(uint160(alice))),
190+
1 ether
191+
);
192+
}
193+
194+
function test_feeRecipient_wrongTokenReverts() public {
195+
MockITokenBridgeEth mockFeeRecipient = new MockITokenBridgeEth();
196+
// Set to return 1 quote but with wrong token (not address(0) which is the native token)
197+
address wrongToken = address(0x456);
198+
mockFeeRecipient.setQuote(1, wrongToken, 0.1 ether);
199+
200+
router.setFeeRecipient(address(mockFeeRecipient));
201+
202+
// Should revert with the fee mismatch error
203+
vm.expectRevert("FungibleTokenRouter: fee must match token");
204+
router.quoteTransferRemote(
205+
destinationDomain,
206+
bytes32(uint256(uint160(alice))),
207+
1 ether
208+
);
209+
}
210+
211+
function test_feeRecipient_correctTokenSucceeds() public {
212+
MockITokenBridgeEth mockFeeRecipient = new MockITokenBridgeEth();
213+
// Set to return 1 quote with correct token (address(0) for native)
214+
mockFeeRecipient.setQuote(1, address(0), 0.1 ether);
215+
216+
router.setFeeRecipient(address(mockFeeRecipient));
217+
218+
// Should succeed and return correct fee
219+
Quote[] memory quotes = router.quoteTransferRemote(
220+
destinationDomain,
221+
bytes32(uint256(uint160(alice))),
222+
1 ether
223+
);
224+
225+
// quotes[1] is the internal fee (amount + fee)
226+
assertEq(quotes[1].amount, 1.1 ether); // 1 ether + 0.1 ether fee
227+
}
116228
}

0 commit comments

Comments
 (0)