UNPKG

@safe-global/safe-contracts

Version:
143 lines (122 loc) 6.94 kB
import { expect } from "chai"; import hre, { deployments, waffle } from "hardhat"; import "@nomiclabs/hardhat-ethers"; import { AddressZero } from "@ethersproject/constants"; import { parseEther } from "@ethersproject/units"; import { defaultAbiCoder } from "@ethersproject/abi"; import { getSafeWithOwners, deployContract, getCompatFallbackHandler } from "../utils/setup"; import { buildSignatureBytes, executeContractCallWithSigners, signHash } from "../../src/utils/execution"; describe("Safe", async () => { const [user1, user2] = waffle.provider.getWallets(); const setupTests = deployments.createFixture(async ({ deployments }) => { await deployments.fixture(); const handler = await getCompatFallbackHandler(); const ownerSafe = await getSafeWithOwners([user1.address, user2.address], 2, handler.address); const messageHandler = handler.attach(ownerSafe.address); return { safe: await getSafeWithOwners([ownerSafe.address, user1.address], 1), ownerSafe, messageHandler, }; }); describe("0xExploit", async () => { /* * In case of 0x it was possible to use EIP-1271 (contract signatures) to generate a valid signature for EOA accounts. * See https://samczsun.com/the-0x-vulnerability-explained/ */ it("should not be able to use EIP-1271 (contract signatures) for EOA", async () => { const { safe, ownerSafe, messageHandler } = await setupTests(); // Safe should be empty again await user1.sendTransaction({ to: safe.address, value: parseEther("1") }); await expect(await hre.ethers.provider.getBalance(safe.address)).to.be.deep.eq(parseEther("1")); const operation = 0; const to = user1.address; const value = parseEther("1"); const data = "0x"; const nonce = await safe.nonce(); // Use off-chain Safe signature const messageData = await safe.encodeTransactionData(to, value, data, operation, 0, 0, 0, AddressZero, AddressZero, nonce); const messageHash = await messageHandler.getMessageHash(messageData); const ownerSigs = await buildSignatureBytes([await signHash(user1, messageHash), await signHash(user2, messageHash)]); const encodedOwnerSigns = defaultAbiCoder.encode(["bytes"], [ownerSigs]).slice(66); // Use EOA owner let sigs = "0x" + "000000000000000000000000" + user2.address.slice(2) + "0000000000000000000000000000000000000000000000000000000000000041" + "00" + // r, s, v encodedOwnerSigns; // Transaction should fail (invalid signatures should revert the Ethereum transaction) await expect( safe.execTransaction(to, value, data, operation, 0, 0, 0, AddressZero, AddressZero, sigs), "Transaction should fail if invalid signature is provided", ).to.be.reverted; await expect(await hre.ethers.provider.getBalance(safe.address)).to.be.deep.eq(parseEther("1")); // Use Safe owner sigs = "0x" + "000000000000000000000000" + ownerSafe.address.slice(2) + "0000000000000000000000000000000000000000000000000000000000000041" + "00" + // r, s, v encodedOwnerSigns; await safe.execTransaction(to, value, data, operation, 0, 0, 0, AddressZero, AddressZero, sigs); // Safe should be empty again await expect(await hre.ethers.provider.getBalance(safe.address)).to.be.deep.eq(parseEther("0")); }); it("should revert if EIP-1271 check changes state", async () => { const { safe, ownerSafe, messageHandler } = await setupTests(); // Test Validator const source = ` contract Test { bool public changeState; uint256 public nonce; function isValidSignature(bytes memory _data, bytes memory _signature) public returns (bytes4) { if (changeState) { nonce = nonce + 1; } return 0x20c13b0b; } function shouldChangeState(bool value) public { changeState = value; } }`; const testValidator = await deployContract(user1, source); await testValidator.shouldChangeState(true); await executeContractCallWithSigners(safe, safe, "addOwnerWithThreshold", [testValidator.address, 1], [user1]); await expect(await safe.getOwners()).to.be.deep.eq([testValidator.address, ownerSafe.address, user1.address]); // Deposit 1 ETH + some spare money for execution await user1.sendTransaction({ to: safe.address, value: parseEther("1") }); await expect(await hre.ethers.provider.getBalance(safe.address)).to.be.deep.eq(parseEther("1")); const operation = 0; const to = user1.address; const value = parseEther("1"); const data = "0x"; const nonce = await safe.nonce(); // Use off-chain Safe signature const messageData = await safe.encodeTransactionData(to, value, data, operation, 0, 0, 0, AddressZero, AddressZero, nonce); const messageHash = await messageHandler.getMessageHash(messageData); const ownerSigs = await buildSignatureBytes([await signHash(user1, messageHash), await signHash(user2, messageHash)]); const encodedOwnerSigns = defaultAbiCoder.encode(["bytes"], [ownerSigs]).slice(66); // Use Safe owner const sigs = "0x" + "000000000000000000000000" + testValidator.address.slice(2) + "0000000000000000000000000000000000000000000000000000000000000041" + "00" + // r, s, v encodedOwnerSigns; // Transaction should fail (state changing signature check should revert) await expect( safe.execTransaction(to, value, data, operation, 0, 0, 0, AddressZero, AddressZero, sigs), "Transaction should fail if invalid signature is provided", ).to.be.reverted; await expect(await hre.ethers.provider.getBalance(safe.address)).to.be.deep.eq(parseEther("1")); await testValidator.shouldChangeState(false); await safe.execTransaction(to, value, data, operation, 0, 0, 0, AddressZero, AddressZero, sigs); // Safe should be empty again await expect(await hre.ethers.provider.getBalance(safe.address)).to.be.deep.eq(parseEther("0")); }); }); });