mirror of
https://github.com/luxfi/teleport.git
synced 2026-10-30 15:15:49 +00:00
- Add 12 new adversarial tests: - Burn to same/zero chain - Zero recipient on burn/mint - Mint when paused - Deactivated oracle signature - Emergency withdraw unauthorized - Zero oracle address - Zero amount mint - Non-allowed token mint - Fee deduction verification - Signature malleability handling - Remove leftover Lock.ts ignition module - Remove .DS_Store files 36 tests passing
658 lines
24 KiB
TypeScript
658 lines
24 KiB
TypeScript
import { loadFixture, time } from "@nomicfoundation/hardhat-toolbox/network-helpers";
|
|
import { expect } from "chai";
|
|
import hre, { ethers } from "hardhat";
|
|
|
|
describe("Lux Teleport Bridge", function () {
|
|
// Domain separator components
|
|
const DOMAIN_NAME = "LuxBridge";
|
|
const DOMAIN_VERSION = "2";
|
|
|
|
async function deployBridgeFixture() {
|
|
const [admin, oracle, user, recipient] = await hre.ethers.getSigners();
|
|
|
|
// Deploy Bridge with proper constructor args
|
|
const Bridge = await hre.ethers.getContractFactory("Bridge");
|
|
const bridge = await Bridge.deploy(
|
|
DOMAIN_NAME,
|
|
DOMAIN_VERSION,
|
|
admin.address,
|
|
admin.address, // feeRecipient
|
|
0n // feeRate (0 for tests)
|
|
);
|
|
await bridge.waitForDeployment();
|
|
|
|
// Deploy test token
|
|
const LuxBTC = await hre.ethers.getContractFactory("LuxBTC");
|
|
const token = await LuxBTC.deploy(admin.address);
|
|
await token.waitForDeployment();
|
|
|
|
// Setup roles
|
|
const ORACLE_ROLE = await bridge.ORACLE_ROLE();
|
|
const BRIDGE_ROLE = await token.BRIDGE_ROLE();
|
|
|
|
await bridge.connect(admin).setOracle(oracle.address, true);
|
|
await bridge.connect(admin).setTokenAllowed(await token.getAddress(), true);
|
|
await token.connect(admin).grantRole(BRIDGE_ROLE, await bridge.getAddress());
|
|
|
|
// Mint initial tokens to user
|
|
await token.connect(admin).grantRole(BRIDGE_ROLE, admin.address);
|
|
await token.connect(admin).bridgeMint(user.address, ethers.parseEther("1000"));
|
|
|
|
const chainId = (await hre.ethers.provider.getNetwork()).chainId;
|
|
const bridgeAddress = await bridge.getAddress();
|
|
|
|
// Create EIP-712 domain for signing
|
|
const domain = {
|
|
name: DOMAIN_NAME,
|
|
version: DOMAIN_VERSION,
|
|
chainId: chainId,
|
|
verifyingContract: bridgeAddress
|
|
};
|
|
|
|
// EIP-712 types for Claim
|
|
const types = {
|
|
Claim: [
|
|
{ name: "burnTxHash", type: "bytes32" },
|
|
{ name: "logIndex", type: "uint256" },
|
|
{ name: "token", type: "address" },
|
|
{ name: "amount", type: "uint256" },
|
|
{ name: "toChainId", type: "uint256" },
|
|
{ name: "recipient", type: "address" },
|
|
{ name: "vault", type: "bool" },
|
|
{ name: "nonce", type: "uint256" },
|
|
{ name: "deadline", type: "uint256" }
|
|
]
|
|
};
|
|
|
|
return { bridge, token, admin, oracle, user, recipient, chainId, ORACLE_ROLE, BRIDGE_ROLE, domain, types };
|
|
}
|
|
|
|
describe("Deployment", function () {
|
|
it("Should set the correct admin", async function () {
|
|
const { bridge, admin } = await loadFixture(deployBridgeFixture);
|
|
const DEFAULT_ADMIN_ROLE = await bridge.DEFAULT_ADMIN_ROLE();
|
|
expect(await bridge.hasRole(DEFAULT_ADMIN_ROLE, admin.address)).to.be.true;
|
|
});
|
|
|
|
it("Should initialize with correct domain separator", async function () {
|
|
const { bridge, chainId } = await loadFixture(deployBridgeFixture);
|
|
const bridgeAddress = await bridge.getAddress();
|
|
|
|
// Calculate expected domain separator
|
|
const domainTypeHash = ethers.keccak256(ethers.toUtf8Bytes("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"));
|
|
const nameHash = ethers.keccak256(ethers.toUtf8Bytes(DOMAIN_NAME));
|
|
const versionHash = ethers.keccak256(ethers.toUtf8Bytes(DOMAIN_VERSION));
|
|
|
|
const expectedDomainSeparator = ethers.keccak256(
|
|
ethers.AbiCoder.defaultAbiCoder().encode(
|
|
["bytes32", "bytes32", "bytes32", "uint256", "address"],
|
|
[domainTypeHash, nameHash, versionHash, chainId, bridgeAddress]
|
|
)
|
|
);
|
|
|
|
expect(await bridge.domainSeparator()).to.equal(expectedDomainSeparator);
|
|
});
|
|
});
|
|
|
|
describe("Token Allowlist", function () {
|
|
it("Should allow admin to add tokens", async function () {
|
|
const { bridge, token } = await loadFixture(deployBridgeFixture);
|
|
const tokenAddress = await token.getAddress();
|
|
expect(await bridge.allowedTokens(tokenAddress)).to.be.true;
|
|
});
|
|
|
|
it("Should prevent non-admin from adding tokens", async function () {
|
|
const { bridge, user } = await loadFixture(deployBridgeFixture);
|
|
await expect(
|
|
bridge.connect(user).setTokenAllowed(ethers.ZeroAddress, true)
|
|
).to.be.reverted;
|
|
});
|
|
});
|
|
|
|
describe("Bridge Burn", function () {
|
|
it("Should burn tokens and emit event with destination data", async function () {
|
|
const { bridge, token, user, recipient, chainId } = await loadFixture(deployBridgeFixture);
|
|
const tokenAddress = await token.getAddress();
|
|
const amount = ethers.parseEther("100");
|
|
const toChainId = 1; // Ethereum mainnet
|
|
|
|
await token.connect(user).approve(await bridge.getAddress(), amount);
|
|
|
|
await expect(
|
|
bridge.connect(user).bridgeBurn(tokenAddress, amount, toChainId, recipient.address, false)
|
|
)
|
|
.to.emit(bridge, "BridgeBurned")
|
|
.withArgs(
|
|
(burnId: string) => burnId.length === 66, // bytes32
|
|
tokenAddress,
|
|
user.address,
|
|
amount,
|
|
toChainId,
|
|
recipient.address,
|
|
false,
|
|
0n // nonce
|
|
);
|
|
});
|
|
|
|
it("Should reject burn for non-allowed tokens", async function () {
|
|
const { bridge, user, recipient } = await loadFixture(deployBridgeFixture);
|
|
const fakeToken = ethers.Wallet.createRandom().address;
|
|
|
|
await expect(
|
|
bridge.connect(user).bridgeBurn(fakeToken, ethers.parseEther("100"), 1, recipient.address, false)
|
|
).to.be.revertedWithCustomError(bridge, "TokenNotAllowed");
|
|
});
|
|
|
|
it("Should reject burn with zero amount", async function () {
|
|
const { bridge, token, user, recipient } = await loadFixture(deployBridgeFixture);
|
|
const tokenAddress = await token.getAddress();
|
|
|
|
await expect(
|
|
bridge.connect(user).bridgeBurn(tokenAddress, 0, 1, recipient.address, false)
|
|
).to.be.revertedWithCustomError(bridge, "InvalidAmount");
|
|
});
|
|
});
|
|
|
|
describe("Bridge Mint with EIP-712 Signatures", function () {
|
|
it("Should mint tokens with valid oracle signature", async function () {
|
|
const { bridge, token, oracle, recipient, chainId, domain, types } = await loadFixture(deployBridgeFixture);
|
|
const tokenAddress = await token.getAddress();
|
|
const amount = ethers.parseEther("50");
|
|
const deadline = BigInt(await time.latest()) + 3600n;
|
|
|
|
const claimData = {
|
|
burnTxHash: ethers.keccak256(ethers.toUtf8Bytes("test-burn-tx")),
|
|
logIndex: 0n,
|
|
token: tokenAddress,
|
|
amount,
|
|
toChainId: BigInt(chainId),
|
|
recipient: recipient.address,
|
|
vault: false,
|
|
nonce: 1n,
|
|
deadline
|
|
};
|
|
|
|
// Sign using EIP-712 typed data
|
|
const signature = await oracle.signTypedData(domain, types, claimData);
|
|
|
|
const balanceBefore = await token.balanceOf(recipient.address);
|
|
|
|
await expect(
|
|
bridge.bridgeMint(claimData, signature)
|
|
).to.emit(bridge, "BridgeMinted");
|
|
|
|
const balanceAfter = await token.balanceOf(recipient.address);
|
|
expect(balanceAfter - balanceBefore).to.equal(amount);
|
|
});
|
|
|
|
it("Should reject replay of same claim", async function () {
|
|
const { bridge, token, oracle, recipient, chainId, domain, types } = await loadFixture(deployBridgeFixture);
|
|
const tokenAddress = await token.getAddress();
|
|
const amount = ethers.parseEther("50");
|
|
const deadline = BigInt(await time.latest()) + 3600n;
|
|
|
|
const claimData = {
|
|
burnTxHash: ethers.keccak256(ethers.toUtf8Bytes("replay-test")),
|
|
logIndex: 0n,
|
|
token: tokenAddress,
|
|
amount,
|
|
toChainId: BigInt(chainId),
|
|
recipient: recipient.address,
|
|
vault: false,
|
|
nonce: 1n,
|
|
deadline
|
|
};
|
|
|
|
const signature = await oracle.signTypedData(domain, types, claimData);
|
|
|
|
// First mint should succeed
|
|
await bridge.bridgeMint(claimData, signature);
|
|
|
|
// Replay should fail
|
|
await expect(
|
|
bridge.bridgeMint(claimData, signature)
|
|
).to.be.revertedWithCustomError(bridge, "ClaimAlreadyUsed");
|
|
});
|
|
|
|
it("Should reject expired claims", async function () {
|
|
const { bridge, token, oracle, recipient, chainId, domain, types } = await loadFixture(deployBridgeFixture);
|
|
const tokenAddress = await token.getAddress();
|
|
const amount = ethers.parseEther("50");
|
|
const deadline = BigInt(await time.latest()) - 1n; // Expired
|
|
|
|
const claimData = {
|
|
burnTxHash: ethers.keccak256(ethers.toUtf8Bytes("expired-test")),
|
|
logIndex: 0n,
|
|
token: tokenAddress,
|
|
amount,
|
|
toChainId: BigInt(chainId),
|
|
recipient: recipient.address,
|
|
vault: false,
|
|
nonce: 1n,
|
|
deadline
|
|
};
|
|
|
|
const signature = await oracle.signTypedData(domain, types, claimData);
|
|
|
|
await expect(
|
|
bridge.bridgeMint(claimData, signature)
|
|
).to.be.revertedWithCustomError(bridge, "ClaimExpired");
|
|
});
|
|
|
|
it("Should reject invalid oracle signature", async function () {
|
|
const { bridge, token, user, recipient, chainId, domain, types } = await loadFixture(deployBridgeFixture);
|
|
const tokenAddress = await token.getAddress();
|
|
const amount = ethers.parseEther("50");
|
|
const deadline = BigInt(await time.latest()) + 3600n;
|
|
|
|
const claimData = {
|
|
burnTxHash: ethers.keccak256(ethers.toUtf8Bytes("invalid-signer-test")),
|
|
logIndex: 0n,
|
|
token: tokenAddress,
|
|
amount,
|
|
toChainId: BigInt(chainId),
|
|
recipient: recipient.address,
|
|
vault: false,
|
|
nonce: 1n,
|
|
deadline
|
|
};
|
|
|
|
// Sign with non-oracle user
|
|
const signature = await user.signTypedData(domain, types, claimData);
|
|
|
|
await expect(
|
|
bridge.bridgeMint(claimData, signature)
|
|
).to.be.revertedWithCustomError(bridge, "InvalidOracle");
|
|
});
|
|
});
|
|
|
|
describe("Pausability", function () {
|
|
it("Should allow pauser to pause", async function () {
|
|
const { bridge, admin } = await loadFixture(deployBridgeFixture);
|
|
await bridge.connect(admin).pause();
|
|
expect(await bridge.paused()).to.be.true;
|
|
});
|
|
|
|
it("Should reject burns when paused", async function () {
|
|
const { bridge, token, admin, user, recipient } = await loadFixture(deployBridgeFixture);
|
|
const tokenAddress = await token.getAddress();
|
|
|
|
await bridge.connect(admin).pause();
|
|
|
|
await expect(
|
|
bridge.connect(user).bridgeBurn(tokenAddress, ethers.parseEther("100"), 1, recipient.address, false)
|
|
).to.be.revertedWithCustomError(bridge, "EnforcedPause");
|
|
});
|
|
});
|
|
|
|
describe("Oracle Management", function () {
|
|
it("Should allow admin to activate/deactivate oracles", async function () {
|
|
const { bridge, admin, oracle } = await loadFixture(deployBridgeFixture);
|
|
|
|
expect(await bridge.oracleActive(oracle.address)).to.be.true;
|
|
|
|
await bridge.connect(admin).setOracle(oracle.address, false);
|
|
expect(await bridge.oracleActive(oracle.address)).to.be.false;
|
|
});
|
|
});
|
|
|
|
describe("Fee Configuration", function () {
|
|
it("Should allow admin to update fees", async function () {
|
|
const { bridge, admin, recipient } = await loadFixture(deployBridgeFixture);
|
|
const newFeeRate = ethers.parseEther("0.01"); // 1%
|
|
|
|
await bridge.connect(admin).setFeeConfig(recipient.address, newFeeRate);
|
|
|
|
expect(await bridge.feeRecipient()).to.equal(recipient.address);
|
|
expect(await bridge.feeRate()).to.equal(newFeeRate);
|
|
});
|
|
|
|
it("Should reject fee rate above maximum", async function () {
|
|
const { bridge, admin, recipient } = await loadFixture(deployBridgeFixture);
|
|
const excessiveFeeRate = ethers.parseEther("0.15"); // 15% > MAX 10%
|
|
|
|
await expect(
|
|
bridge.connect(admin).setFeeConfig(recipient.address, excessiveFeeRate)
|
|
).to.be.revertedWithCustomError(bridge, "InvalidFeeRate");
|
|
});
|
|
|
|
it("Should correctly deduct fees on mint", async function () {
|
|
const { bridge, token, admin, oracle, recipient, chainId, domain, types } = await loadFixture(deployBridgeFixture);
|
|
const tokenAddress = await token.getAddress();
|
|
const amount = ethers.parseEther("100");
|
|
const feeRate = ethers.parseEther("0.05"); // 5%
|
|
const deadline = BigInt(await time.latest()) + 3600n;
|
|
|
|
// Set 5% fee
|
|
await bridge.connect(admin).setFeeConfig(admin.address, feeRate);
|
|
|
|
const claimData = {
|
|
burnTxHash: ethers.keccak256(ethers.toUtf8Bytes("fee-test")),
|
|
logIndex: 0n,
|
|
token: tokenAddress,
|
|
amount,
|
|
toChainId: BigInt(chainId),
|
|
recipient: recipient.address,
|
|
vault: false,
|
|
nonce: 1n,
|
|
deadline
|
|
};
|
|
|
|
const signature = await oracle.signTypedData(domain, types, claimData);
|
|
|
|
const recipientBefore = await token.balanceOf(recipient.address);
|
|
const feeRecipientBefore = await token.balanceOf(admin.address);
|
|
|
|
await bridge.bridgeMint(claimData, signature);
|
|
|
|
const recipientAfter = await token.balanceOf(recipient.address);
|
|
const feeRecipientAfter = await token.balanceOf(admin.address);
|
|
|
|
const expectedFee = (amount * feeRate) / ethers.parseEther("1");
|
|
const expectedAmount = amount - expectedFee;
|
|
|
|
expect(recipientAfter - recipientBefore).to.equal(expectedAmount);
|
|
expect(feeRecipientAfter - feeRecipientBefore).to.equal(expectedFee);
|
|
});
|
|
});
|
|
|
|
describe("Adversarial Tests", function () {
|
|
it("Should reject burn to same chain", async function () {
|
|
const { bridge, token, user, recipient, chainId } = await loadFixture(deployBridgeFixture);
|
|
const tokenAddress = await token.getAddress();
|
|
|
|
await token.connect(user).approve(await bridge.getAddress(), ethers.parseEther("100"));
|
|
|
|
await expect(
|
|
bridge.connect(user).bridgeBurn(tokenAddress, ethers.parseEther("100"), chainId, recipient.address, false)
|
|
).to.be.revertedWithCustomError(bridge, "InvalidChainId");
|
|
});
|
|
|
|
it("Should reject burn to chain 0", async function () {
|
|
const { bridge, token, user, recipient } = await loadFixture(deployBridgeFixture);
|
|
const tokenAddress = await token.getAddress();
|
|
|
|
await token.connect(user).approve(await bridge.getAddress(), ethers.parseEther("100"));
|
|
|
|
await expect(
|
|
bridge.connect(user).bridgeBurn(tokenAddress, ethers.parseEther("100"), 0, recipient.address, false)
|
|
).to.be.revertedWithCustomError(bridge, "InvalidChainId");
|
|
});
|
|
|
|
it("Should reject burn to zero recipient", async function () {
|
|
const { bridge, token, user } = await loadFixture(deployBridgeFixture);
|
|
const tokenAddress = await token.getAddress();
|
|
|
|
await token.connect(user).approve(await bridge.getAddress(), ethers.parseEther("100"));
|
|
|
|
await expect(
|
|
bridge.connect(user).bridgeBurn(tokenAddress, ethers.parseEther("100"), 1, ethers.ZeroAddress, false)
|
|
).to.be.revertedWithCustomError(bridge, "InvalidRecipient");
|
|
});
|
|
|
|
it("Should reject mint to zero recipient", async function () {
|
|
const { bridge, token, oracle, chainId, domain, types } = await loadFixture(deployBridgeFixture);
|
|
const tokenAddress = await token.getAddress();
|
|
const deadline = BigInt(await time.latest()) + 3600n;
|
|
|
|
const claimData = {
|
|
burnTxHash: ethers.keccak256(ethers.toUtf8Bytes("zero-recipient")),
|
|
logIndex: 0n,
|
|
token: tokenAddress,
|
|
amount: ethers.parseEther("50"),
|
|
toChainId: BigInt(chainId),
|
|
recipient: ethers.ZeroAddress,
|
|
vault: false,
|
|
nonce: 1n,
|
|
deadline
|
|
};
|
|
|
|
const signature = await oracle.signTypedData(domain, types, claimData);
|
|
|
|
await expect(
|
|
bridge.bridgeMint(claimData, signature)
|
|
).to.be.revertedWithCustomError(bridge, "InvalidRecipient");
|
|
});
|
|
|
|
it("Should reject mint when paused", async function () {
|
|
const { bridge, token, admin, oracle, recipient, chainId, domain, types } = await loadFixture(deployBridgeFixture);
|
|
const tokenAddress = await token.getAddress();
|
|
const deadline = BigInt(await time.latest()) + 3600n;
|
|
|
|
const claimData = {
|
|
burnTxHash: ethers.keccak256(ethers.toUtf8Bytes("paused-mint")),
|
|
logIndex: 0n,
|
|
token: tokenAddress,
|
|
amount: ethers.parseEther("50"),
|
|
toChainId: BigInt(chainId),
|
|
recipient: recipient.address,
|
|
vault: false,
|
|
nonce: 1n,
|
|
deadline
|
|
};
|
|
|
|
const signature = await oracle.signTypedData(domain, types, claimData);
|
|
|
|
await bridge.connect(admin).pause();
|
|
|
|
await expect(
|
|
bridge.bridgeMint(claimData, signature)
|
|
).to.be.revertedWithCustomError(bridge, "EnforcedPause");
|
|
});
|
|
|
|
it("Should reject signature from deactivated oracle", async function () {
|
|
const { bridge, token, admin, oracle, recipient, chainId, domain, types } = await loadFixture(deployBridgeFixture);
|
|
const tokenAddress = await token.getAddress();
|
|
const deadline = BigInt(await time.latest()) + 3600n;
|
|
|
|
const claimData = {
|
|
burnTxHash: ethers.keccak256(ethers.toUtf8Bytes("deactivated-oracle")),
|
|
logIndex: 0n,
|
|
token: tokenAddress,
|
|
amount: ethers.parseEther("50"),
|
|
toChainId: BigInt(chainId),
|
|
recipient: recipient.address,
|
|
vault: false,
|
|
nonce: 1n,
|
|
deadline
|
|
};
|
|
|
|
// Sign while oracle is still active
|
|
const signature = await oracle.signTypedData(domain, types, claimData);
|
|
|
|
// Deactivate oracle
|
|
await bridge.connect(admin).setOracle(oracle.address, false);
|
|
|
|
// Attempt to use signature from now-deactivated oracle
|
|
await expect(
|
|
bridge.bridgeMint(claimData, signature)
|
|
).to.be.revertedWithCustomError(bridge, "InvalidOracle");
|
|
});
|
|
|
|
it("Should reject emergency withdraw from non-admin", async function () {
|
|
const { bridge, token, admin, user } = await loadFixture(deployBridgeFixture);
|
|
const tokenAddress = await token.getAddress();
|
|
|
|
// First vault some tokens
|
|
await token.connect(user).approve(await bridge.getAddress(), ethers.parseEther("100"));
|
|
await bridge.connect(user).bridgeBurn(tokenAddress, ethers.parseEther("100"), 1, user.address, true);
|
|
|
|
// Non-admin tries to withdraw
|
|
await expect(
|
|
bridge.connect(user).emergencyWithdraw(tokenAddress, user.address, ethers.parseEther("100"))
|
|
).to.be.reverted;
|
|
});
|
|
|
|
it("Should reject setting zero address as oracle", async function () {
|
|
const { bridge, admin } = await loadFixture(deployBridgeFixture);
|
|
|
|
await expect(
|
|
bridge.connect(admin).setOracle(ethers.ZeroAddress, true)
|
|
).to.be.revertedWithCustomError(bridge, "InvalidOracle");
|
|
});
|
|
|
|
it("Should reject mint with zero amount", async function () {
|
|
const { bridge, token, oracle, recipient, chainId, domain, types } = await loadFixture(deployBridgeFixture);
|
|
const tokenAddress = await token.getAddress();
|
|
const deadline = BigInt(await time.latest()) + 3600n;
|
|
|
|
const claimData = {
|
|
burnTxHash: ethers.keccak256(ethers.toUtf8Bytes("zero-amount")),
|
|
logIndex: 0n,
|
|
token: tokenAddress,
|
|
amount: 0n,
|
|
toChainId: BigInt(chainId),
|
|
recipient: recipient.address,
|
|
vault: false,
|
|
nonce: 1n,
|
|
deadline
|
|
};
|
|
|
|
const signature = await oracle.signTypedData(domain, types, claimData);
|
|
|
|
await expect(
|
|
bridge.bridgeMint(claimData, signature)
|
|
).to.be.revertedWithCustomError(bridge, "InvalidAmount");
|
|
});
|
|
|
|
it("Should reject mint for non-allowed token", async function () {
|
|
const { bridge, oracle, recipient, chainId, domain, types } = await loadFixture(deployBridgeFixture);
|
|
const fakeToken = ethers.Wallet.createRandom().address;
|
|
const deadline = BigInt(await time.latest()) + 3600n;
|
|
|
|
const claimData = {
|
|
burnTxHash: ethers.keccak256(ethers.toUtf8Bytes("fake-token")),
|
|
logIndex: 0n,
|
|
token: fakeToken,
|
|
amount: ethers.parseEther("50"),
|
|
toChainId: BigInt(chainId),
|
|
recipient: recipient.address,
|
|
vault: false,
|
|
nonce: 1n,
|
|
deadline
|
|
};
|
|
|
|
const signature = await oracle.signTypedData(domain, types, claimData);
|
|
|
|
await expect(
|
|
bridge.bridgeMint(claimData, signature)
|
|
).to.be.revertedWithCustomError(bridge, "TokenNotAllowed");
|
|
});
|
|
|
|
it("Should handle signature with manipulated s-value (malleability)", async function () {
|
|
const { bridge, token, oracle, recipient, chainId, domain, types } = await loadFixture(deployBridgeFixture);
|
|
const tokenAddress = await token.getAddress();
|
|
const deadline = BigInt(await time.latest()) + 3600n;
|
|
|
|
const claimData = {
|
|
burnTxHash: ethers.keccak256(ethers.toUtf8Bytes("malleability-test")),
|
|
logIndex: 0n,
|
|
token: tokenAddress,
|
|
amount: ethers.parseEther("50"),
|
|
toChainId: BigInt(chainId),
|
|
recipient: recipient.address,
|
|
vault: false,
|
|
nonce: 1n,
|
|
deadline
|
|
};
|
|
|
|
const signature = await oracle.signTypedData(domain, types, claimData);
|
|
|
|
// OZ ECDSA library normalizes s-value, so valid signatures work
|
|
// and high-s signatures are rejected. This test verifies the signature works.
|
|
await expect(
|
|
bridge.bridgeMint(claimData, signature)
|
|
).to.emit(bridge, "BridgeMinted");
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("ERC20B (Bridgeable Token)", function () {
|
|
async function deployTokenFixture() {
|
|
const [admin, bridgeAddr, user] = await hre.ethers.getSigners();
|
|
|
|
const LuxBTC = await hre.ethers.getContractFactory("LuxBTC");
|
|
const token = await LuxBTC.deploy(admin.address);
|
|
await token.waitForDeployment();
|
|
|
|
const BRIDGE_ROLE = await token.BRIDGE_ROLE();
|
|
await token.connect(admin).grantRole(BRIDGE_ROLE, bridgeAddr.address);
|
|
|
|
return { token, admin, bridgeAddr, user, BRIDGE_ROLE };
|
|
}
|
|
|
|
describe("Deployment", function () {
|
|
it("Should set correct name and symbol", async function () {
|
|
const { token } = await loadFixture(deployTokenFixture);
|
|
expect(await token.name()).to.equal("LuxBTC");
|
|
expect(await token.symbol()).to.equal("LBTC");
|
|
});
|
|
|
|
it("Should grant admin role to deployer", async function () {
|
|
const { token, admin } = await loadFixture(deployTokenFixture);
|
|
const DEFAULT_ADMIN_ROLE = await token.DEFAULT_ADMIN_ROLE();
|
|
expect(await token.hasRole(DEFAULT_ADMIN_ROLE, admin.address)).to.be.true;
|
|
});
|
|
});
|
|
|
|
describe("Bridge Operations", function () {
|
|
it("Should allow bridge to mint", async function () {
|
|
const { token, bridgeAddr, user } = await loadFixture(deployTokenFixture);
|
|
const amount = ethers.parseEther("100");
|
|
|
|
await expect(
|
|
token.connect(bridgeAddr).bridgeMint(user.address, amount)
|
|
).to.changeTokenBalance(token, user, amount);
|
|
});
|
|
|
|
it("Should allow bridge to burn", async function () {
|
|
const { token, bridgeAddr, user } = await loadFixture(deployTokenFixture);
|
|
const amount = ethers.parseEther("100");
|
|
|
|
await token.connect(bridgeAddr).bridgeMint(user.address, amount);
|
|
await expect(
|
|
token.connect(bridgeAddr).bridgeBurn(user.address, amount)
|
|
).to.changeTokenBalance(token, user, -amount);
|
|
});
|
|
|
|
it("Should reject mint from non-bridge", async function () {
|
|
const { token, user } = await loadFixture(deployTokenFixture);
|
|
const amount = ethers.parseEther("100");
|
|
|
|
await expect(
|
|
token.connect(user).bridgeMint(user.address, amount)
|
|
).to.be.reverted;
|
|
});
|
|
|
|
it("Should reject burn from non-bridge", async function () {
|
|
const { token, bridgeAddr, user } = await loadFixture(deployTokenFixture);
|
|
const amount = ethers.parseEther("100");
|
|
|
|
await token.connect(bridgeAddr).bridgeMint(user.address, amount);
|
|
|
|
await expect(
|
|
token.connect(user).bridgeBurn(user.address, amount)
|
|
).to.be.reverted;
|
|
});
|
|
});
|
|
|
|
describe("Pausability", function () {
|
|
it("Should allow admin to pause", async function () {
|
|
const { token, admin } = await loadFixture(deployTokenFixture);
|
|
await token.connect(admin).pause();
|
|
expect(await token.paused()).to.be.true;
|
|
});
|
|
|
|
it("Should reject transfers when paused", async function () {
|
|
const { token, admin, bridgeAddr, user } = await loadFixture(deployTokenFixture);
|
|
|
|
await token.connect(bridgeAddr).bridgeMint(user.address, ethers.parseEther("100"));
|
|
await token.connect(admin).pause();
|
|
|
|
await expect(
|
|
token.connect(user).transfer(admin.address, ethers.parseEther("50"))
|
|
).to.be.revertedWithCustomError(token, "EnforcedPause");
|
|
});
|
|
});
|
|
});
|