Skip to content

Commit 32d72c0

Browse files
committed
test: add unit tests for linea adapter and spoke
1 parent b6812a5 commit 32d72c0

File tree

2 files changed

+369
-0
lines changed

2 files changed

+369
-0
lines changed
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
import { amountToLp, mockTreeRoot, refundProposalLiveness, bondAmount } from "../constants";
2+
import {
3+
ethers,
4+
expect,
5+
Contract,
6+
FakeContract,
7+
SignerWithAddress,
8+
getContractFactory,
9+
seedWallet,
10+
randomAddress,
11+
toWei,
12+
BigNumber,
13+
} from "../../utils/utils";
14+
import { hubPoolFixture, enableTokensForLP } from "../fixtures/HubPool.Fixture";
15+
import { constructSingleChainTree } from "../MerkleLib.utils";
16+
import { smock } from "@defi-wonderland/smock";
17+
18+
let hubPool: Contract,
19+
lineaAdapter: Contract,
20+
weth: Contract,
21+
dai: Contract,
22+
usdc: Contract,
23+
timer: Contract,
24+
mockSpoke: Contract;
25+
let l2Weth: string, l2Dai: string, l2Usdc: string;
26+
let owner: SignerWithAddress, dataWorker: SignerWithAddress, liquidityProvider: SignerWithAddress;
27+
let lineaMessageService: FakeContract, lineaTokenBridge: FakeContract, lineaUsdcBridge: FakeContract;
28+
29+
const lineaChainId = 59144;
30+
31+
const lineaMessageServiceAbi = [
32+
{
33+
inputs: [
34+
{ internalType: "address", name: "_to", type: "address" },
35+
{ internalType: "uint256", name: "_fee", type: "uint256" },
36+
{ internalType: "bytes", name: "_calldata", type: "bytes" },
37+
],
38+
name: "sendMessage",
39+
outputs: [],
40+
stateMutability: "payable",
41+
type: "function",
42+
},
43+
];
44+
45+
const lineaTokenBridgeAbi = [
46+
{
47+
inputs: [
48+
{ internalType: "address", name: "_token", type: "address" },
49+
{ internalType: "uint256", name: "_amount", type: "uint256" },
50+
{ internalType: "address", name: "_recipient", type: "address" },
51+
],
52+
name: "bridgeToken",
53+
outputs: [],
54+
stateMutability: "payable",
55+
type: "function",
56+
},
57+
];
58+
59+
const lineaUsdcBridgeAbi = [
60+
{
61+
inputs: [
62+
{ internalType: "uint256", name: "amount", type: "uint256" },
63+
{ internalType: "address", name: "to", type: "address" },
64+
],
65+
name: "depositTo",
66+
outputs: [],
67+
stateMutability: "payable",
68+
type: "function",
69+
},
70+
{
71+
inputs: [],
72+
name: "usdc",
73+
outputs: [
74+
{
75+
name: "",
76+
type: "address",
77+
},
78+
],
79+
stateMutability: "view",
80+
type: "function",
81+
},
82+
];
83+
84+
describe("Linea Chain Adapter", function () {
85+
beforeEach(async function () {
86+
[owner, dataWorker, liquidityProvider] = await ethers.getSigners();
87+
({ weth, dai, usdc, l2Weth, l2Dai, l2Usdc, hubPool, mockSpoke, timer } = await hubPoolFixture());
88+
await seedWallet(dataWorker, [dai, usdc], weth, amountToLp);
89+
await seedWallet(liquidityProvider, [dai, usdc], weth, amountToLp.mul(10));
90+
91+
await enableTokensForLP(owner, hubPool, weth, [weth, dai, usdc]);
92+
await weth.connect(liquidityProvider).approve(hubPool.address, amountToLp);
93+
await hubPool.connect(liquidityProvider).addLiquidity(weth.address, amountToLp);
94+
await weth.connect(dataWorker).approve(hubPool.address, bondAmount.mul(10));
95+
await dai.connect(liquidityProvider).approve(hubPool.address, amountToLp);
96+
await hubPool.connect(liquidityProvider).addLiquidity(dai.address, amountToLp);
97+
await dai.connect(dataWorker).approve(hubPool.address, bondAmount.mul(10));
98+
await usdc.connect(liquidityProvider).approve(hubPool.address, amountToLp);
99+
await hubPool.connect(liquidityProvider).addLiquidity(usdc.address, amountToLp);
100+
await usdc.connect(dataWorker).approve(hubPool.address, bondAmount.mul(10));
101+
102+
lineaMessageService = await smock.fake(lineaMessageServiceAbi, {
103+
address: "0xd19d4B5d358258f05D7B411E21A1460D11B0876F",
104+
});
105+
lineaTokenBridge = await smock.fake(lineaTokenBridgeAbi, { address: "0x051F1D88f0aF5763fB888eC4378b4D8B29ea3319" });
106+
lineaUsdcBridge = await smock.fake(lineaUsdcBridgeAbi, {
107+
address: "0x504a330327a089d8364c4ab3811ee26976d388ce",
108+
});
109+
lineaUsdcBridge.usdc.returns(usdc.address);
110+
111+
lineaAdapter = await (
112+
await getContractFactory("Linea_Adapter", owner)
113+
).deploy(weth.address, lineaMessageService.address, lineaTokenBridge.address, lineaUsdcBridge.address);
114+
115+
// Seed the HubPool some funds so it can send L1->L2 messages.
116+
await hubPool.connect(liquidityProvider).loadEthForL2Calls({ value: toWei("100000") });
117+
118+
await hubPool.setCrossChainContracts(lineaChainId, lineaAdapter.address, mockSpoke.address);
119+
await hubPool.setPoolRebalanceRoute(lineaChainId, weth.address, l2Weth);
120+
await hubPool.setPoolRebalanceRoute(lineaChainId, dai.address, l2Dai);
121+
await hubPool.setPoolRebalanceRoute(lineaChainId, usdc.address, l2Usdc);
122+
});
123+
124+
it("relayMessage calls spoke pool functions", async function () {
125+
const newAdmin = randomAddress();
126+
const functionCallData = mockSpoke.interface.encodeFunctionData("setCrossDomainAdmin", [newAdmin]);
127+
expect(await hubPool.relaySpokePoolAdminFunction(lineaChainId, functionCallData))
128+
.to.emit(lineaAdapter.attach(hubPool.address), "MessageRelayed")
129+
.withArgs(mockSpoke.address, functionCallData);
130+
expect(lineaMessageService.sendMessage).to.have.been.calledWith(mockSpoke.address, 0, functionCallData);
131+
expect(lineaMessageService.sendMessage).to.have.been.calledWithValue(BigNumber.from(0));
132+
});
133+
it("Correctly calls appropriate bridge functions when making ERC20 cross chain calls", async function () {
134+
// Create an action that will send an L1->L2 tokens transfer and bundle. For this, create a relayer repayment bundle
135+
// and check that at it's finalization the L2 bridge contracts are called as expected.
136+
const { leaves, tree, tokensSendToL2 } = await constructSingleChainTree(dai.address, 1, lineaChainId);
137+
await hubPool.connect(dataWorker).proposeRootBundle([3117], 1, tree.getHexRoot(), mockTreeRoot, mockTreeRoot);
138+
await timer.setCurrentTime(Number(await timer.getCurrentTime()) + refundProposalLiveness + 1);
139+
await hubPool.connect(dataWorker).executeRootBundle(...Object.values(leaves[0]), tree.getHexProof(leaves[0]));
140+
141+
// The correct functions should have been called on the optimism contracts.
142+
const expectedErc20L1ToL2BridgeParams = [dai.address, tokensSendToL2, mockSpoke.address];
143+
expect(lineaTokenBridge.bridgeToken).to.have.been.calledWith(...expectedErc20L1ToL2BridgeParams);
144+
});
145+
it("Correctly calls appropriate bridge functions when making USDC cross chain calls", async function () {
146+
// Create an action that will send an L1->L2 tokens transfer and bundle. For this, create a relayer repayment bundle
147+
// and check that at it's finalization the L2 bridge contracts are called as expected.
148+
const { leaves, tree, tokensSendToL2 } = await constructSingleChainTree(usdc.address, 1, lineaChainId);
149+
await hubPool.connect(dataWorker).proposeRootBundle([3117], 1, tree.getHexRoot(), mockTreeRoot, mockTreeRoot);
150+
await timer.setCurrentTime(Number(await timer.getCurrentTime()) + refundProposalLiveness + 1);
151+
await hubPool.connect(dataWorker).executeRootBundle(...Object.values(leaves[0]), tree.getHexProof(leaves[0]));
152+
153+
// The correct functions should have been called on the optimism contracts.
154+
const expectedErc20L1ToL2BridgeParams = [tokensSendToL2, mockSpoke.address];
155+
expect(lineaUsdcBridge.depositTo).to.have.been.calledWith(...expectedErc20L1ToL2BridgeParams);
156+
});
157+
it("Correctly unwraps WETH and bridges ETH", async function () {
158+
const { leaves, tree } = await constructSingleChainTree(weth.address, 1, lineaChainId);
159+
160+
await hubPool.connect(dataWorker).proposeRootBundle([3117], 1, tree.getHexRoot(), mockTreeRoot, mockTreeRoot);
161+
await timer.setCurrentTime(Number(await timer.getCurrentTime()) + refundProposalLiveness + 1);
162+
163+
// Since WETH is used as proposal bond, the bond plus the WETH are debited from the HubPool's balance.
164+
// The WETH used in the Linea_Adapter is withdrawn to ETH and then paid to the Linea MessageService.
165+
const proposalBond = await hubPool.bondAmount();
166+
await expect(() =>
167+
hubPool.connect(dataWorker).executeRootBundle(...Object.values(leaves[0]), tree.getHexProof(leaves[0]))
168+
).to.changeTokenBalance(weth, hubPool, leaves[0].netSendAmounts[0].add(proposalBond).mul(-1));
169+
expect(lineaMessageService.sendMessage).to.have.been.calledWith(mockSpoke.address, 0, "0x");
170+
expect(lineaMessageService.sendMessage).to.have.been.calledWithValue(leaves[0].netSendAmounts[0]);
171+
});
172+
});
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
import { mockTreeRoot, amountToReturn, amountHeldByPool } from "../constants";
2+
import {
3+
ethers,
4+
expect,
5+
Contract,
6+
FakeContract,
7+
SignerWithAddress,
8+
getContractFactory,
9+
seedContract,
10+
} from "../../utils/utils";
11+
import { hre } from "../../utils/utils.hre";
12+
13+
import { hubPoolFixture } from "../fixtures/HubPool.Fixture";
14+
import { constructSingleRelayerRefundTree } from "../MerkleLib.utils";
15+
import { smock } from "@defi-wonderland/smock";
16+
17+
let hubPool: Contract, lineaSpokePool: Contract, dai: Contract, weth: Contract, usdc: Contract;
18+
let owner: SignerWithAddress, relayer: SignerWithAddress, rando: SignerWithAddress;
19+
let lineaMessageService: FakeContract, lineaTokenBridge: FakeContract, lineaUsdcBridge: FakeContract;
20+
21+
const lineaMessageServiceAbi = [
22+
{
23+
inputs: [
24+
{ internalType: "address", name: "_to", type: "address" },
25+
{ internalType: "uint256", name: "_fee", type: "uint256" },
26+
{ internalType: "bytes", name: "_calldata", type: "bytes" },
27+
],
28+
name: "sendMessage",
29+
outputs: [],
30+
stateMutability: "payable",
31+
type: "function",
32+
},
33+
{
34+
inputs: [],
35+
name: "sender",
36+
outputs: [
37+
{
38+
name: "",
39+
type: "address",
40+
},
41+
],
42+
stateMutability: "view",
43+
type: "function",
44+
},
45+
];
46+
47+
const lineaTokenBridgeAbi = [
48+
{
49+
inputs: [
50+
{ internalType: "address", name: "_token", type: "address" },
51+
{ internalType: "uint256", name: "_amount", type: "uint256" },
52+
{ internalType: "address", name: "_recipient", type: "address" },
53+
],
54+
name: "bridgeToken",
55+
outputs: [],
56+
stateMutability: "payable",
57+
type: "function",
58+
},
59+
];
60+
61+
const lineaUsdcBridgeAbi = [
62+
{
63+
inputs: [
64+
{ internalType: "uint256", name: "amount", type: "uint256" },
65+
{ internalType: "address", name: "to", type: "address" },
66+
],
67+
name: "depositTo",
68+
outputs: [],
69+
stateMutability: "payable",
70+
type: "function",
71+
},
72+
{
73+
inputs: [],
74+
name: "usdc",
75+
outputs: [
76+
{
77+
name: "",
78+
type: "address",
79+
},
80+
],
81+
stateMutability: "view",
82+
type: "function",
83+
},
84+
];
85+
86+
describe("Linea Spoke Pool", function () {
87+
beforeEach(async function () {
88+
[owner, relayer, rando] = await ethers.getSigners();
89+
({ weth, dai, usdc, hubPool } = await hubPoolFixture());
90+
91+
lineaMessageService = await smock.fake(lineaMessageServiceAbi, {
92+
address: "0x508Ca82Df566dCD1B0DE8296e70a96332cD644ec",
93+
});
94+
lineaMessageService.sender.reset();
95+
lineaTokenBridge = await smock.fake(lineaTokenBridgeAbi, { address: "0x353012dc4a9A6cF55c941bADC267f82004A8ceB9" });
96+
lineaUsdcBridge = await smock.fake(lineaUsdcBridgeAbi, {
97+
address: "0xA2Ee6Fce4ACB62D95448729cDb781e3BEb62504A",
98+
});
99+
lineaUsdcBridge.usdc.returns(usdc.address);
100+
101+
lineaSpokePool = await hre.upgrades.deployProxy(
102+
await getContractFactory("Linea_SpokePool", owner),
103+
[
104+
0,
105+
lineaMessageService.address,
106+
lineaTokenBridge.address,
107+
lineaUsdcBridge.address,
108+
owner.address,
109+
hubPool.address,
110+
],
111+
{ kind: "uups", unsafeAllow: ["delegatecall"], constructorArgs: [weth.address, 60 * 60, 9 * 60 * 60] }
112+
);
113+
114+
await seedContract(lineaSpokePool, relayer, [dai, usdc], weth, amountHeldByPool);
115+
});
116+
117+
it("Only cross domain owner upgrade logic contract", async function () {
118+
const implementation = await hre.upgrades.deployImplementation(await getContractFactory("Linea_SpokePool", owner), {
119+
kind: "uups",
120+
unsafeAllow: ["delegatecall"],
121+
constructorArgs: [weth.address, 60 * 60, 9 * 60 * 60],
122+
});
123+
124+
// upgradeTo fails unless called by cross domain admin
125+
await expect(lineaSpokePool.connect(rando).upgradeTo(implementation)).to.be.revertedWith(
126+
"ONLY_COUNTERPART_GATEWAY"
127+
);
128+
lineaMessageService.sender.returns(owner.address);
129+
await lineaSpokePool.connect(owner).upgradeTo(implementation);
130+
});
131+
it("Only cross domain owner can set l2MessageService", async function () {
132+
await expect(lineaSpokePool.setL2MessageService(rando.address)).to.be.reverted;
133+
lineaMessageService.sender.returns(owner.address);
134+
await lineaSpokePool.connect(owner).setL2MessageService(rando.address);
135+
expect(await lineaSpokePool.l2MessageService()).to.equal(rando.address);
136+
});
137+
it("Only cross domain owner can set l2TokenBridge", async function () {
138+
await expect(lineaSpokePool.setL2TokenBridge(rando.address)).to.be.reverted;
139+
lineaMessageService.sender.returns(owner.address);
140+
await lineaSpokePool.connect(owner).setL2TokenBridge(rando.address);
141+
expect(await lineaSpokePool.l2TokenBridge()).to.equal(rando.address);
142+
});
143+
it("Only cross domain owner can set l2UsdcBridge", async function () {
144+
await expect(lineaSpokePool.setL2UsdcBridge(rando.address)).to.be.reverted;
145+
lineaMessageService.sender.returns(owner.address);
146+
await lineaSpokePool.connect(owner).setL2UsdcBridge(rando.address);
147+
expect(await lineaSpokePool.l2UsdcBridge()).to.equal(rando.address);
148+
});
149+
it("Only cross domain owner can relay admin root bundles", async function () {
150+
const { tree } = await constructSingleRelayerRefundTree(dai.address, await lineaSpokePool.callStatic.chainId());
151+
await expect(lineaSpokePool.relayRootBundle(tree.getHexRoot(), mockTreeRoot)).to.be.revertedWith(
152+
"ONLY_COUNTERPART_GATEWAY"
153+
);
154+
});
155+
it("Bridge tokens to hub pool correctly calls the L2 Token Bridge for ERC20", async function () {
156+
console.log("l2Dai", await lineaSpokePool.callStatic.chainId());
157+
const { leaves, tree } = await constructSingleRelayerRefundTree(
158+
dai.address,
159+
await lineaSpokePool.callStatic.chainId()
160+
);
161+
lineaMessageService.sender.returns(owner.address);
162+
await lineaSpokePool.connect(owner).relayRootBundle(tree.getHexRoot(), mockTreeRoot);
163+
lineaMessageService.sender.reset();
164+
await lineaSpokePool.connect(relayer).executeRelayerRefundLeaf(0, leaves[0], tree.getHexProof(leaves[0]));
165+
166+
// This should have sent tokens back to L1. Check the correct methods on the gateway are correctly called.
167+
expect(lineaTokenBridge.bridgeToken).to.have.been.calledWith(dai.address, amountToReturn, hubPool.address);
168+
});
169+
it("Bridge USDC to hub pool correctly calls the L2 USDC Bridge", async function () {
170+
const { leaves, tree } = await constructSingleRelayerRefundTree(
171+
usdc.address,
172+
await lineaSpokePool.callStatic.chainId()
173+
);
174+
lineaMessageService.sender.returns(owner.address);
175+
await lineaSpokePool.connect(owner).relayRootBundle(tree.getHexRoot(), mockTreeRoot);
176+
await lineaSpokePool.connect(relayer).executeRelayerRefundLeaf(0, leaves[0], tree.getHexProof(leaves[0]));
177+
178+
// This should have sent tokens back to L1. Check the correct methods on the gateway are correctly called.
179+
expect(lineaUsdcBridge.depositTo).to.have.been.calledWith(amountToReturn, hubPool.address);
180+
});
181+
it("Bridge ETH to hub pool correctly calls the Standard L2 Bridge for WETH, including unwrap", async function () {
182+
const { leaves, tree } = await constructSingleRelayerRefundTree(
183+
weth.address,
184+
await lineaSpokePool.callStatic.chainId()
185+
);
186+
lineaMessageService.sender.returns(owner.address);
187+
await lineaSpokePool.connect(owner).relayRootBundle(tree.getHexRoot(), mockTreeRoot);
188+
189+
// Executing the refund leaf should cause spoke pool to unwrap WETH to ETH to prepare to send it as msg.value
190+
// to the ERC20 bridge. This results in a net decrease in WETH balance.
191+
await expect(() =>
192+
lineaSpokePool.connect(relayer).executeRelayerRefundLeaf(0, leaves[0], tree.getHexProof(leaves[0]))
193+
).to.changeTokenBalance(weth, lineaSpokePool, amountToReturn.mul(-1));
194+
expect(lineaMessageService.sendMessage).to.have.been.calledWith(hubPool.address, 0, "0x");
195+
expect(lineaMessageService.sendMessage).to.have.been.calledWithValue(amountToReturn);
196+
});
197+
});

0 commit comments

Comments
 (0)