mirror of
https://github.com/scroll-tech/scroll.git
synced 2026-04-23 03:00:50 -04:00
330 lines
12 KiB
TypeScript
330 lines
12 KiB
TypeScript
/* eslint-disable node/no-unpublished-import */
|
|
/* eslint-disable node/no-missing-import */
|
|
import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers";
|
|
import { expect } from "chai";
|
|
import { MaxUint256, Signature, ZeroAddress, ZeroHash, toBigInt } from "ethers";
|
|
import { ethers } from "hardhat";
|
|
|
|
import { GasSwap, ERC2771Forwarder, MockERC20, MockGasSwapTarget } from "../typechain";
|
|
|
|
describe("GasSwap.spec", async () => {
|
|
let deployer: HardhatEthersSigner;
|
|
let signer: HardhatEthersSigner;
|
|
|
|
let forwarder: ERC2771Forwarder;
|
|
let swap: GasSwap;
|
|
let target: MockGasSwapTarget;
|
|
let token: MockERC20;
|
|
|
|
beforeEach(async () => {
|
|
[deployer, signer] = await ethers.getSigners();
|
|
|
|
const ERC2771Forwarder = await ethers.getContractFactory("ERC2771Forwarder", deployer);
|
|
forwarder = await ERC2771Forwarder.deploy("ERC2771Forwarder");
|
|
|
|
const GasSwap = await ethers.getContractFactory("GasSwap", deployer);
|
|
swap = await GasSwap.deploy(forwarder.getAddress());
|
|
|
|
const MockGasSwapTarget = await ethers.getContractFactory("MockGasSwapTarget", deployer);
|
|
target = await MockGasSwapTarget.deploy();
|
|
|
|
const MockERC20 = await ethers.getContractFactory("MockERC20", deployer);
|
|
token = await MockERC20.deploy("x", "y", 18);
|
|
});
|
|
|
|
context("auth", async () => {
|
|
it("should initialize correctly", async () => {
|
|
expect(await swap.owner()).to.eq(deployer.address);
|
|
});
|
|
|
|
context("#updateFeeRatio", async () => {
|
|
it("should revert, when non-owner call", async () => {
|
|
await expect(swap.connect(signer).updateFeeRatio(1)).to.revertedWith("Ownable: caller is not the owner");
|
|
});
|
|
|
|
it("should succeed", async () => {
|
|
expect(await swap.feeRatio()).to.eq(ZeroAddress);
|
|
await expect(swap.updateFeeRatio(100)).to.emit(swap, "UpdateFeeRatio").withArgs(100);
|
|
expect(await swap.feeRatio()).to.eq(100);
|
|
});
|
|
});
|
|
|
|
context("#updateApprovedTarget", async () => {
|
|
it("should revert, when non-owner call", async () => {
|
|
await expect(swap.connect(signer).updateApprovedTarget(target.getAddress(), false)).to.revertedWith(
|
|
"Ownable: caller is not the owner"
|
|
);
|
|
});
|
|
|
|
it("should succeed", async () => {
|
|
expect(await swap.approvedTargets(target.getAddress())).to.eq(false);
|
|
await expect(swap.updateApprovedTarget(target.getAddress(), true))
|
|
.to.emit(swap, "UpdateApprovedTarget")
|
|
.withArgs(await target.getAddress(), true);
|
|
expect(await swap.approvedTargets(target.getAddress())).to.eq(true);
|
|
await expect(swap.updateApprovedTarget(target.getAddress(), false))
|
|
.to.emit(swap, "UpdateApprovedTarget")
|
|
.withArgs(await target.getAddress(), false);
|
|
expect(await swap.approvedTargets(target.getAddress())).to.eq(false);
|
|
});
|
|
});
|
|
|
|
context("#withdraw", async () => {
|
|
it("should revert, when non-owner call", async () => {
|
|
await expect(swap.connect(signer).withdraw(ZeroAddress, 0)).to.revertedWith("Ownable: caller is not the owner");
|
|
});
|
|
|
|
it("should succeed, when withdraw ETH", async () => {
|
|
await deployer.sendTransaction({ to: swap.getAddress(), value: ethers.parseEther("1") });
|
|
const balanceBefore = await ethers.provider.getBalance(deployer.address);
|
|
const tx = await swap.withdraw(ZeroAddress, ethers.parseEther("1"));
|
|
const receipt = await tx.wait();
|
|
const balanceAfter = await ethers.provider.getBalance(deployer.address);
|
|
expect(balanceAfter - balanceBefore).to.eq(ethers.parseEther("1") - receipt!.gasUsed * receipt!.gasPrice);
|
|
});
|
|
|
|
it("should succeed, when withdraw token", async () => {
|
|
await token.mint(swap.getAddress(), ethers.parseEther("1"));
|
|
const balanceBefore = await token.balanceOf(deployer.address);
|
|
await swap.withdraw(token.getAddress(), ethers.parseEther("1"));
|
|
const balanceAfter = await token.balanceOf(deployer.address);
|
|
expect(balanceAfter - balanceBefore).to.eq(ethers.parseEther("1"));
|
|
});
|
|
});
|
|
});
|
|
|
|
const permit = async (amount: bigint) => {
|
|
const value = {
|
|
owner: signer.address,
|
|
spender: await swap.getAddress(),
|
|
value: amount,
|
|
nonce: await token.nonces(signer.address),
|
|
deadline: MaxUint256,
|
|
};
|
|
|
|
const domain = {
|
|
name: await token.name(),
|
|
version: "1",
|
|
chainId: (await ethers.provider.getNetwork()).chainId,
|
|
verifyingContract: await token.getAddress(),
|
|
};
|
|
|
|
const types = {
|
|
Permit: [
|
|
{
|
|
name: "owner",
|
|
type: "address",
|
|
},
|
|
{
|
|
name: "spender",
|
|
type: "address",
|
|
},
|
|
{
|
|
name: "value",
|
|
type: "uint256",
|
|
},
|
|
{
|
|
name: "nonce",
|
|
type: "uint256",
|
|
},
|
|
{
|
|
name: "deadline",
|
|
type: "uint256",
|
|
},
|
|
],
|
|
};
|
|
|
|
const signature = Signature.from(await signer.signTypedData(domain, types, value));
|
|
return signature;
|
|
};
|
|
|
|
context("swap", async () => {
|
|
it("should revert, when target not approved", async () => {
|
|
await expect(
|
|
swap.swap(
|
|
{
|
|
token: token.getAddress(),
|
|
value: 0,
|
|
deadline: 0,
|
|
r: ZeroHash,
|
|
s: ZeroHash,
|
|
v: 0,
|
|
},
|
|
{
|
|
target: target.getAddress(),
|
|
data: "0x",
|
|
minOutput: 0,
|
|
}
|
|
)
|
|
).to.revertedWith("target not approved");
|
|
});
|
|
|
|
it("should revert, when insufficient output amount", async () => {
|
|
const amountIn = ethers.parseEther("1");
|
|
const amountOut = ethers.parseEther("2");
|
|
await token.mint(signer.address, amountIn);
|
|
await deployer.sendTransaction({ to: target.getAddress(), value: amountOut });
|
|
const signature = await permit(amountIn);
|
|
|
|
await target.setToken(token.getAddress());
|
|
await target.setAmountIn(amountIn);
|
|
|
|
await swap.updateApprovedTarget(target.getAddress(), true);
|
|
await expect(
|
|
swap.connect(signer).swap(
|
|
{
|
|
token: await token.getAddress(),
|
|
value: amountIn,
|
|
deadline: MaxUint256,
|
|
r: signature.r,
|
|
s: signature.s,
|
|
v: signature.v,
|
|
},
|
|
{
|
|
target: target.getAddress(),
|
|
data: "0x8119c065",
|
|
minOutput: amountOut + 1n,
|
|
}
|
|
)
|
|
).to.revertedWith("insufficient output amount");
|
|
});
|
|
|
|
for (const refundRatio of [0n, 1n, 5n]) {
|
|
for (const feeRatio of ["0", "5", "50"]) {
|
|
it(`should succeed, when swap by signer directly, with feeRatio[${feeRatio}%] refundRatio[${refundRatio}%]`, async () => {
|
|
const amountIn = ethers.parseEther("1");
|
|
const amountOut = ethers.parseEther("2");
|
|
await token.mint(signer.address, amountIn);
|
|
await deployer.sendTransaction({ to: target.getAddress(), value: amountOut });
|
|
const signature = await permit(amountIn);
|
|
|
|
await target.setToken(token.getAddress());
|
|
await target.setAmountIn(amountIn);
|
|
await target.setRefund((amountIn * refundRatio) / 100n);
|
|
|
|
await swap.updateApprovedTarget(target.getAddress(), true);
|
|
await swap.updateFeeRatio(ethers.parseEther(feeRatio) / 100n);
|
|
const fee = (amountOut * toBigInt(feeRatio)) / 100n;
|
|
|
|
const balanceBefore = await ethers.provider.getBalance(signer.address);
|
|
const tx = await swap.connect(signer).swap(
|
|
{
|
|
token: await token.getAddress(),
|
|
value: amountIn,
|
|
deadline: MaxUint256,
|
|
r: signature.r,
|
|
s: signature.s,
|
|
v: signature.v,
|
|
},
|
|
{
|
|
target: target.getAddress(),
|
|
data: "0x8119c065",
|
|
minOutput: amountOut - fee,
|
|
}
|
|
);
|
|
const receipt = await tx.wait();
|
|
const balanceAfter = await ethers.provider.getBalance(signer.address);
|
|
expect(balanceAfter - balanceBefore).to.eq(amountOut - fee - receipt!.gasUsed * receipt!.gasPrice);
|
|
expect(await token.balanceOf(signer.address)).to.eq((amountIn * refundRatio) / 100n);
|
|
});
|
|
|
|
it(`should succeed, when swap by signer with forwarder, with feeRatio[${feeRatio}%] refundRatio[${refundRatio}%]`, async () => {
|
|
const amountIn = ethers.parseEther("1");
|
|
const amountOut = ethers.parseEther("2");
|
|
await token.mint(signer.address, amountIn);
|
|
await deployer.sendTransaction({ to: await target.getAddress(), value: amountOut });
|
|
const permitSignature = await permit(amountIn);
|
|
|
|
await target.setToken(token.getAddress());
|
|
await target.setAmountIn(amountIn);
|
|
await target.setRefund((amountIn * refundRatio) / 100n);
|
|
|
|
await swap.updateApprovedTarget(target.getAddress(), true);
|
|
await swap.updateFeeRatio(ethers.parseEther(feeRatio) / 100n);
|
|
const fee = (amountOut * toBigInt(feeRatio)) / 100n;
|
|
|
|
const reqWithoutSignature = {
|
|
from: signer.address,
|
|
to: await swap.getAddress(),
|
|
value: 0n,
|
|
gas: 1000000,
|
|
nonce: await forwarder.nonces(signer.address),
|
|
deadline: 2000000000,
|
|
data: swap.interface.encodeFunctionData("swap", [
|
|
{
|
|
token: await token.getAddress(),
|
|
value: amountIn,
|
|
deadline: MaxUint256,
|
|
r: permitSignature.r,
|
|
s: permitSignature.s,
|
|
v: permitSignature.v,
|
|
},
|
|
{
|
|
target: await target.getAddress(),
|
|
data: "0x8119c065",
|
|
minOutput: amountOut - fee,
|
|
},
|
|
]),
|
|
};
|
|
|
|
const signature = await signer.signTypedData(
|
|
{
|
|
name: "ERC2771Forwarder",
|
|
version: "1",
|
|
chainId: (await ethers.provider.getNetwork()).chainId,
|
|
verifyingContract: await forwarder.getAddress(),
|
|
},
|
|
{
|
|
ForwardRequest: [
|
|
{
|
|
name: "from",
|
|
type: "address",
|
|
},
|
|
{
|
|
name: "to",
|
|
type: "address",
|
|
},
|
|
{
|
|
name: "value",
|
|
type: "uint256",
|
|
},
|
|
{
|
|
name: "gas",
|
|
type: "uint256",
|
|
},
|
|
{
|
|
name: "nonce",
|
|
type: "uint256",
|
|
},
|
|
{
|
|
name: "deadline",
|
|
type: "uint48",
|
|
},
|
|
{
|
|
name: "data",
|
|
type: "bytes",
|
|
},
|
|
],
|
|
},
|
|
reqWithoutSignature
|
|
);
|
|
|
|
const balanceBefore = await ethers.provider.getBalance(signer.address);
|
|
await forwarder.execute({
|
|
from: reqWithoutSignature.from,
|
|
to: reqWithoutSignature.to,
|
|
value: reqWithoutSignature.value,
|
|
gas: reqWithoutSignature.gas,
|
|
deadline: reqWithoutSignature.deadline,
|
|
data: reqWithoutSignature.data,
|
|
signature,
|
|
});
|
|
const balanceAfter = await ethers.provider.getBalance(signer.address);
|
|
expect(balanceAfter - balanceBefore).to.eq(amountOut - fee);
|
|
expect(await token.balanceOf(signer.address)).to.eq((amountIn * refundRatio) / 100n);
|
|
});
|
|
}
|
|
}
|
|
});
|
|
});
|