Skip to content

Commit cd0ba78

Browse files
fix: Pay in LZToken (SC-1152) (#41)
* fix * add: param * add: tests * fix: review
1 parent 80e689e commit cd0ba78

File tree

5 files changed

+168
-8
lines changed

5 files changed

+168
-8
lines changed

src/forwarders/LZForwarder.sol

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
// SPDX-License-Identifier: AGPL-3.0-or-later
22
pragma solidity ^0.8.0;
33

4+
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
5+
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
6+
47
struct MessagingParams {
58
uint32 dstEid;
69
bytes32 receiver;
@@ -21,10 +24,12 @@ struct MessagingFee {
2124
}
2225

2326
interface ILayerZeroEndpointV2 {
27+
function lzToken() external view returns (address);
2428
function send(
2529
MessagingParams calldata _params,
2630
address _refundAddress
2731
) external payable returns (MessagingReceipt memory);
32+
function setLzToken(address _lzToken) external;
2833
function quote(
2934
MessagingParams calldata _params,
3035
address _sender
@@ -33,6 +38,8 @@ interface ILayerZeroEndpointV2 {
3338

3439
library LZForwarder {
3540

41+
error LzTokenUnavailable();
42+
3643
uint32 public constant ENDPOINT_ID_BASE = 30184;
3744
uint32 public constant ENDPOINT_ID_BNB = 30102;
3845
uint32 public constant ENDPOINT_ID_ETHEREUM = 30101;
@@ -51,19 +58,30 @@ library LZForwarder {
5158
ILayerZeroEndpointV2 endpoint,
5259
bytes memory _message,
5360
bytes memory _options,
54-
address _refundAddress
61+
address _refundAddress,
62+
bool _payInLzToken
5563
) internal {
5664
MessagingParams memory params = MessagingParams({
5765
dstEid: _dstEid,
5866
receiver: _receiver,
5967
message: _message,
6068
options: _options,
61-
payInLzToken: false
69+
payInLzToken: _payInLzToken
6270
});
6371

6472
MessagingFee memory fee = endpoint.quote(params, address(this));
73+
if (fee.lzTokenFee > 0) _payLzToken(endpoint, fee.lzTokenFee);
6574

6675
endpoint.send{ value: fee.nativeFee }(params, _refundAddress);
6776
}
6877

78+
function _payLzToken(ILayerZeroEndpointV2 endpoint, uint256 _lzTokenFee) internal {
79+
// @dev Cannot cache the token because it is not immutable in the endpoint.
80+
address lzToken = endpoint.lzToken();
81+
if (lzToken == address(0)) revert LzTokenUnavailable();
82+
83+
// Pay LZ token fee by sending tokens to the endpoint.
84+
SafeERC20.safeTransfer(IERC20(lzToken), address(endpoint), _lzTokenFee);
85+
}
86+
6987
}

src/receivers/LZReceiver.sol

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,13 @@ pragma solidity ^0.8.0;
44
import { Address } from "openzeppelin-contracts/contracts/utils/Address.sol";
55
import { Ownable } from "openzeppelin-contracts/contracts/access/Ownable.sol";
66

7-
import { OApp, Origin } from "layerzerolabs/oapp-evm/contracts/oapp/OApp.sol";
7+
import { OAppReceiver, Origin, OAppCore } from "layerzerolabs/oapp-evm/contracts/oapp/OApp.sol";
88

99
/**
1010
* @title LZReceiver
1111
* @notice Receive messages from LayerZero-style bridge.
1212
*/
13-
contract LZReceiver is OApp {
13+
contract LZReceiver is OAppReceiver {
1414

1515
using Address for address;
1616

@@ -27,7 +27,7 @@ contract LZReceiver is OApp {
2727
address _target,
2828
address _delegate,
2929
address _owner
30-
) OApp(_destinationEndpoint, _delegate) Ownable(_owner) {
30+
) OAppCore(_destinationEndpoint, _delegate) Ownable(_owner) {
3131
target = _target;
3232
sourceAuthority = _sourceAuthority;
3333
srcEid = _srcEid;

test/IntegrationBase.t.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ abstract contract IntegrationBaseTest is Test {
4747

4848
Bridge bridge;
4949

50-
function setUp() public {
50+
function setUp() public virtual {
5151
source = getChain("mainnet").createFork();
5252
}
5353

test/LZIntegration.t.sol

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,8 @@ contract LZIntegrationTest is IntegrationBaseTest {
199199
ILayerZeroEndpointV2(bridge.sourceCrossChainMessenger),
200200
message,
201201
options,
202-
sourceAuthority
202+
sourceAuthority,
203+
false
203204
);
204205
}
205206

@@ -214,7 +215,8 @@ contract LZIntegrationTest is IntegrationBaseTest {
214215
ILayerZeroEndpointV2(bridge.destinationCrossChainMessenger),
215216
message,
216217
options,
217-
destinationAuthority
218+
destinationAuthority,
219+
false
218220
);
219221
}
220222

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
// SPDX-License-Identifier: AGPL-3.0-or-later
2+
pragma solidity >=0.8.0;
3+
4+
import { IERC20 } from "forge-std/interfaces/IERC20.sol";
5+
6+
import { OptionsBuilder } from "layerzerolabs/oapp-evm/contracts/oapp/libs/OptionsBuilder.sol";
7+
8+
import { LZBridgeTesting } from "src/testing/bridges/LZBridgeTesting.sol";
9+
import { LZForwarder, ILayerZeroEndpointV2 } from "src/forwarders/LZForwarder.sol";
10+
import { LZReceiver, Origin } from "src/receivers/LZReceiver.sol";
11+
import { RecordedLogs } from "src/testing/utils/RecordedLogs.sol";
12+
13+
import "./IntegrationBase.t.sol";
14+
15+
interface ITreasury {
16+
function setLzTokenEnabled(bool _lzTokenEnabled) external;
17+
function setLzTokenFee(uint256 _lzTokenFee) external;
18+
}
19+
20+
contract LZIntegrationTestWithLZToken is IntegrationBaseTest {
21+
22+
using DomainHelpers for *;
23+
using LZBridgeTesting for *;
24+
using OptionsBuilder for bytes;
25+
26+
uint32 sourceEndpointId = LZForwarder.ENDPOINT_ID_ETHEREUM;
27+
uint32 destinationEndpointId;
28+
29+
address sourceEndpoint = LZForwarder.ENDPOINT_ETHEREUM;
30+
address destinationEndpoint;
31+
32+
address lzToken = 0x6985884C4392D348587B19cb9eAAf157F13271cd;
33+
address lzOwner = 0xBe010A7e3686FdF65E93344ab664D065A0B02478;
34+
address treasury = 0x5ebB3f2feaA15271101a927869B3A56837e73056;
35+
36+
Domain destination2;
37+
Bridge bridge2;
38+
39+
function setUp() public override {
40+
super.setUp();
41+
42+
source.selectFork();
43+
44+
vm.startPrank(lzOwner);
45+
ILayerZeroEndpointV2(sourceEndpoint).setLzToken(lzToken);
46+
ITreasury(treasury).setLzTokenEnabled(true);
47+
ITreasury(treasury).setLzTokenFee(1e18);
48+
vm.stopPrank();
49+
}
50+
51+
function test_base() public {
52+
destinationEndpointId = LZForwarder.ENDPOINT_ID_BASE;
53+
destinationEndpoint = LZForwarder.ENDPOINT_BASE;
54+
55+
runCrossChainTests(getChain("base").createFork());
56+
}
57+
58+
function test_binance() public {
59+
destinationEndpointId = LZForwarder.ENDPOINT_ID_BNB;
60+
destinationEndpoint = LZForwarder.ENDPOINT_BNB;
61+
62+
runCrossChainTests(getChain("bnb_smart_chain").createFork());
63+
}
64+
65+
function initSourceReceiver() internal override returns (address) {
66+
return address(new LZReceiver(
67+
sourceEndpoint,
68+
destinationEndpointId,
69+
bytes32(uint256(uint160(destinationAuthority))),
70+
address(moSource),
71+
makeAddr("delegate"),
72+
makeAddr("owner")
73+
));
74+
}
75+
76+
function initDestinationReceiver() internal override returns (address) {
77+
return address(new LZReceiver(
78+
destinationEndpoint,
79+
sourceEndpointId,
80+
bytes32(uint256(uint160(sourceAuthority))),
81+
address(moDestination),
82+
makeAddr("delegate"),
83+
makeAddr("owner")
84+
));
85+
}
86+
87+
function initBridgeTesting() internal override returns (Bridge memory) {
88+
return LZBridgeTesting.createLZBridge(source, destination);
89+
}
90+
91+
function queueSourceToDestination(bytes memory message) internal override {
92+
// Gas to queue message
93+
vm.deal(sourceAuthority, 1 ether);
94+
deal(lzToken, sourceAuthority, 1 ether);
95+
96+
bytes memory options = OptionsBuilder.newOptions().addExecutorLzReceiveOption(200_000, 0);
97+
98+
assertEq(IERC20(lzToken).balanceOf(address(sourceAuthority)), 1 ether);
99+
assertEq(address(sourceAuthority).balance, 1 ether);
100+
101+
LZForwarder.sendMessage(
102+
destinationEndpointId,
103+
bytes32(uint256(uint160(destinationReceiver))),
104+
ILayerZeroEndpointV2(bridge.sourceCrossChainMessenger),
105+
message,
106+
options,
107+
sourceAuthority,
108+
true
109+
);
110+
111+
// LZ token and ETH spent
112+
assertLt(IERC20(lzToken).balanceOf(address(sourceAuthority)), 1 ether);
113+
assertLt(address(sourceAuthority).balance, 1 ether);
114+
}
115+
116+
function queueDestinationToSource(bytes memory message) internal override {
117+
vm.deal(destinationAuthority, 1 ether); // Gas to queue message
118+
119+
bytes memory options = OptionsBuilder.newOptions().addExecutorLzReceiveOption(200_000, 0);
120+
121+
LZForwarder.sendMessage(
122+
sourceEndpointId,
123+
bytes32(uint256(uint160(sourceReceiver))),
124+
ILayerZeroEndpointV2(bridge.destinationCrossChainMessenger),
125+
message,
126+
options,
127+
destinationAuthority,
128+
false
129+
);
130+
}
131+
132+
function relaySourceToDestination() internal override {
133+
bridge.relayMessagesToDestination(true, sourceAuthority, destinationReceiver);
134+
}
135+
136+
function relayDestinationToSource() internal override {
137+
bridge.relayMessagesToSource(true, destinationAuthority, sourceReceiver);
138+
}
139+
140+
}

0 commit comments

Comments
 (0)