Skip to content

Commit e7b5091

Browse files
authored
require that returned lzTokenFee is zero. Add tests for fee cap checking (#1029)
Signed-off-by: Ihor Farion <ihor@umaproject.org>
1 parent e1e0034 commit e7b5091

File tree

4 files changed

+107
-7
lines changed

4 files changed

+107
-7
lines changed

contracts/libraries/OFTTransportAdapter.sol

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ contract OFTTransportAdapter {
3333

3434
error OftFeeCapExceeded();
3535
error OftInsufficientBalanceForFee();
36+
error OftLzFeeNotZero();
3637
error OftIncorrectAmountReceivedLD();
3738
error OftIncorrectAmountSentLD();
3839

@@ -88,6 +89,7 @@ contract OFTTransportAdapter {
8889
uint256 nativeFee = fee.nativeFee;
8990
if (nativeFee > OFT_FEE_CAP) revert OftFeeCapExceeded();
9091
if (nativeFee > address(this).balance) revert OftInsufficientBalanceForFee();
92+
if (fee.lzTokenFee != 0) revert OftLzFeeNotZero();
9193

9294
// Approve the exact _amount for `_messenger` to spend. Fee will be paid in native token
9395
_token.forceApprove(address(_messenger), _amount);

contracts/test/MockOFTMessenger.sol

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,21 @@ import "../interfaces/IOFT.sol";
1010
*/
1111
contract MockOFTMessenger is IOFT {
1212
address public token;
13+
uint256 nativeFee;
14+
uint256 lzFee;
1315

14-
constructor(address _token) {
16+
constructor(
17+
address _token,
18+
uint256 _nativeFee,
19+
uint256 _lzFee
20+
) {
1521
token = _token;
22+
nativeFee = _nativeFee;
23+
lzFee = _lzFee;
1624
}
1725

1826
function quoteSend(SendParam calldata _sendParam, bool _payInLzToken) external view returns (MessagingFee memory) {
19-
return MessagingFee(0, 0);
27+
return MessagingFee(nativeFee, lzFee);
2028
}
2129

2230
function send(

test/evm/foundry/local/Universal_Adapter.t.sol

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { AdapterStore, MessengerTypes } from "../../../../contracts/AdapterStore
1212
import { IOFT, SendParam, MessagingFee } from "../../../../contracts/interfaces/IOFT.sol";
1313
import { MockOFTMessenger } from "../../../../contracts/test/MockOFTMessenger.sol";
1414
import { AddressToBytes32 } from "../../../../contracts/libraries/AddressConverters.sol";
15+
import { OFTTransportAdapter } from "../../../../contracts/libraries/OFTTransportAdapter.sol";
1516

1617
contract UniversalAdapterTest is Test {
1718
using AddressToBytes32 for address;
@@ -23,7 +24,6 @@ contract UniversalAdapterTest is Test {
2324
uint256 relayRootBundleNonce = 0;
2425
address relayRootBundleTargetAddress = address(0);
2526
AdapterStore adapterStore;
26-
IOFT oftMessenger;
2727
ERC20 usdc;
2828
ERC20 usdt;
2929
uint256 usdcMintAmount = 100e6;
@@ -57,8 +57,6 @@ contract UniversalAdapterTest is Test {
5757
store = new HubPoolStore(address(hubPool));
5858
usdc = new ERC20("USDC", "USDC");
5959
usdt = new ERC20("USDT", "USDT");
60-
oftMessenger = IOFT(new MockOFTMessenger(address(usdt)));
61-
adapterStore.setMessenger(MessengerTypes.OFT_MESSENGER, oftDstEid, address(usdt), address(oftMessenger));
6260
MockCCTPMinter minter = new MockCCTPMinter();
6361
cctpMessenger = new MockCCTPMessenger(ITokenMinter(minter));
6462
adapter = new Universal_Adapter(
@@ -273,6 +271,11 @@ contract UniversalAdapterTest is Test {
273271
}
274272

275273
function testRelayTokens_oft() public {
274+
vm.startPrank(owner);
275+
IOFT oftMessenger = IOFT(new MockOFTMessenger(address(usdt), 0, 0));
276+
adapterStore.setMessenger(MessengerTypes.OFT_MESSENGER, oftDstEid, address(usdt), address(oftMessenger));
277+
vm.stopPrank();
278+
276279
// Uses OFT to send USDT
277280
vm.expectCall(
278281
address(oftMessenger),
@@ -296,6 +299,47 @@ contract UniversalAdapterTest is Test {
296299
hubPool.relayTokens(address(usdt), makeAddr("l2Usdt"), usdcMintAmount, spokePoolTarget);
297300
}
298301

302+
function testNonZeroLzFee() public {
303+
vm.startPrank(owner);
304+
// Mock an OFT messenger that returns a non-zero lzTokenFee
305+
IOFT oftMessengerWithNonZeroLzFee = IOFT(new MockOFTMessenger(address(usdt), 0, 1)); // nativeFee = 0, lzFee = 1
306+
adapterStore.setMessenger(
307+
MessengerTypes.OFT_MESSENGER,
308+
oftDstEid,
309+
address(usdt),
310+
address(oftMessengerWithNonZeroLzFee)
311+
);
312+
vm.stopPrank();
313+
314+
// Expect the OftLzFeeNotZero error from OFTTransportAdapter logic within Universal_Adapter
315+
// This will be caught by HubPool and re-thrown. Due to `revert_strings = "strip"`,
316+
// HubPool's require(..., "string") will revert with no data.
317+
vm.expectRevert();
318+
hubPool.relayTokens(address(usdt), makeAddr("l2Usdt"), usdcMintAmount, spokePoolTarget);
319+
}
320+
321+
function testFeeTooHigh() public {
322+
// Determine a native fee that is higher than the adapter's OFT_FEE_CAP
323+
uint256 highNativeFee = adapter.OFT_FEE_CAP() + 1;
324+
MockOFTMessenger oftMessengerWithHighFee = new MockOFTMessenger(address(usdt), highNativeFee, 0); // nativeFee > OFT_FEE_CAP, lzFee = 0
325+
vm.startPrank(owner);
326+
adapterStore.setMessenger(
327+
MessengerTypes.OFT_MESSENGER,
328+
oftDstEid,
329+
address(usdt),
330+
address(oftMessengerWithHighFee)
331+
);
332+
vm.stopPrank();
333+
334+
deal(address(hubPool), adapter.OFT_FEE_CAP());
335+
336+
// Expect the OftFeeCapExceeded error from OFTTransportAdapter logic within Universal_Adapter
337+
// This will be caught by HubPool and re-thrown. Due to `revert_strings = "strip"`,
338+
// HubPool's require(..., "string") will revert with no data.
339+
vm.expectRevert();
340+
hubPool.relayTokens(address(usdt), makeAddr("l2Usdt"), usdcMintAmount, spokePoolTarget);
341+
}
342+
299343
function testRelayTokens_default() public {
300344
vm.expectRevert();
301345
hubPool.relayTokens(makeAddr("erc20"), makeAddr("l2Erc20"), usdcMintAmount, spokePoolTarget);

test/evm/foundry/local/Universal_SpokePool.t.sol

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import "../../../../contracts/test/MockCCTP.sol";
1111
import { IOFT, SendParam, MessagingFee } from "../../../../contracts/interfaces/IOFT.sol";
1212
import { MockOFTMessenger } from "../../../../contracts/test/MockOFTMessenger.sol";
1313
import { AddressToBytes32 } from "../../../../contracts/libraries/AddressConverters.sol";
14+
import { OFTTransportAdapter } from "../../../../contracts/libraries/OFTTransportAdapter.sol";
1415

1516
contract MockHelios is IHelios {
1617
mapping(bytes32 => bytes32) public storageSlots;
@@ -70,7 +71,6 @@ contract UniversalSpokePoolTest is Test {
7071
using AddressToBytes32 for address;
7172
MockUniversalSpokePool spokePool;
7273
MockHelios helios;
73-
IOFT oftMessenger;
7474

7575
address hubPoolStore;
7676
address hubPool;
@@ -111,7 +111,6 @@ contract UniversalSpokePoolTest is Test {
111111
new ERC1967Proxy(address(spokePool), abi.encodeCall(Universal_SpokePool.initialize, (0, hubPool, hubPool)))
112112
);
113113
spokePool = MockUniversalSpokePool(payable(proxy));
114-
oftMessenger = IOFT(new MockOFTMessenger(address(usdt)));
115114
deal(address(usdc), address(spokePool), usdcMintAmount, true);
116115
}
117116

@@ -294,6 +293,7 @@ contract UniversalSpokePoolTest is Test {
294293
}
295294

296295
function testSetOftMessenger() public {
296+
IOFT oftMessenger = IOFT(new MockOFTMessenger(address(usdt), 0, 0));
297297
bytes memory message = abi.encodeWithSignature(
298298
"setOftMessenger(address,address)",
299299
address(usdt),
@@ -305,7 +305,53 @@ contract UniversalSpokePoolTest is Test {
305305
assertEq(spokePool.oftMessengers(address(usdt)), address(oftMessenger));
306306
}
307307

308+
function testNonZeroLzFee() public {
309+
// Mock an OFT messenger that returns a non-zero lzTokenFee
310+
MockOFTMessenger oftMessengerWithNonZeroLzFee = new MockOFTMessenger(address(usdt), 0, 1); // nativeFee = 0, lzFee = 1
311+
312+
// Set this messenger for USDT
313+
bytes memory message = abi.encodeWithSignature(
314+
"setOftMessenger(address,address)",
315+
address(usdt),
316+
address(oftMessengerWithNonZeroLzFee)
317+
);
318+
bytes memory value = abi.encode(address(spokePool), message);
319+
helios.updateStorageSlot(spokePool.getSlotKey(nonce), keccak256(value));
320+
spokePool.executeMessage(nonce, value, 100);
321+
nonce++; // Increment nonce for the next message
322+
323+
// Expect the OftLzFeeNotZero error from OFTTransportAdapter
324+
vm.expectRevert(OFTTransportAdapter.OftLzFeeNotZero.selector);
325+
spokePool.test_bridgeTokensToHubPool(usdcMintAmount, address(usdt));
326+
}
327+
328+
function testFeeTooHigh() public {
329+
// Mock an OFT messenger that returns a nativeFee higher than OFT_FEE_CAP
330+
uint256 highNativeFee = spokePool.OFT_FEE_CAP() + 1;
331+
MockOFTMessenger oftMessengerWithHighFee = new MockOFTMessenger(address(usdt), highNativeFee, 0); // nativeFee > OFT_FEE_CAP, lzFee = 0
332+
333+
// Set this messenger for USDT
334+
bytes memory message = abi.encodeWithSignature(
335+
"setOftMessenger(address,address)",
336+
address(usdt),
337+
address(oftMessengerWithHighFee)
338+
);
339+
bytes memory value = abi.encode(address(spokePool), message);
340+
helios.updateStorageSlot(spokePool.getSlotKey(nonce), keccak256(value));
341+
spokePool.executeMessage(nonce, value, 100);
342+
nonce++; // Increment nonce for the next message
343+
344+
// Fund the spokePool with enough native currency to attempt the transaction but less than the high fee
345+
// The check for OFT_FEE_CAP happens before the balance check.
346+
deal(address(spokePool), spokePool.OFT_FEE_CAP());
347+
348+
// Expect the OftFeeCapExceeded error from OFTTransportAdapter
349+
vm.expectRevert(OFTTransportAdapter.OftFeeCapExceeded.selector);
350+
spokePool.test_bridgeTokensToHubPool(usdcMintAmount, address(usdt));
351+
}
352+
308353
function testBridgeTokensToHubPool_oft() public {
354+
IOFT oftMessenger = IOFT(new MockOFTMessenger(address(usdt), 0, 0));
309355
bytes memory message = abi.encodeWithSignature(
310356
"setOftMessenger(address,address)",
311357
address(usdt),

0 commit comments

Comments
 (0)