import { expect } from "chai"; import hre, { deployments, waffle } from "hardhat"; import "@nomiclabs/hardhat-ethers"; import { deployContract, getSafeWithOwners } from "../utils/setup"; import { safeApproveHash, buildSignatureBytes, executeContractCallWithSigners, buildSafeTransaction, executeTx, calculateSafeTransactionHash, buildContractCall } from "../../src/utils/execution"; import { parseEther } from "@ethersproject/units"; import { chainId } from "../utils/encoding"; describe("GnosisSafe", async () => { const [user1, user2] = waffle.provider.getWallets(); const setupTests = deployments.createFixture(async ({ deployments }) => { await deployments.fixture(); const setterSource = ` contract StorageSetter { function setStorage(bytes3 data) public { bytes32 slot = 0x4242424242424242424242424242424242424242424242424242424242424242; // solhint-disable-next-line no-inline-assembly assembly { sstore(slot, data) } } }` const storageSetter = await deployContract(user1, setterSource); const reverterSource = ` contract Reverter { function revert() public { require(false, "Shit happens"); } }` const reverter = await deployContract(user1, reverterSource); return { safe: await getSafeWithOwners([user1.address]), reverter, storageSetter } }) describe("execTransaction", async () => { it('should revert if too little gas is provided', async () => { const { safe } = await setupTests() const tx = buildSafeTransaction({ to: safe.address, safeTxGas: 1000000, nonce: await safe.nonce() }) const signatureBytes = buildSignatureBytes([await safeApproveHash(user1, safe, tx, true)]) await expect( safe.execTransaction( tx.to, tx.value, tx.data, tx.operation, tx.safeTxGas, tx.baseGas, tx.gasPrice, tx.gasToken, tx.refundReceiver, signatureBytes, { gasLimit: 1000000 } ) ).to.be.revertedWith("GS010") }) it('should emit event for successful call execution', async () => { const { safe, storageSetter } = await setupTests() const txHash = calculateSafeTransactionHash(safe, buildContractCall(storageSetter, "setStorage", ["0xbaddad"], await safe.nonce()), await chainId()) await expect( executeContractCallWithSigners(safe, storageSetter, "setStorage", ["0xbaddad"], [user1]) ).to.emit(safe, "ExecutionSuccess").withArgs(txHash, 0) await expect( await hre.ethers.provider.getStorageAt(safe.address, "0x4242424242424242424242424242424242424242424242424242424242424242") ).to.be.eq("0x" + "".padEnd(64, "0")) await expect( await hre.ethers.provider.getStorageAt(storageSetter.address, "0x4242424242424242424242424242424242424242424242424242424242424242") ).to.be.eq("0x" + "baddad".padEnd(64, "0")) }) it('should emit event for failed call execution if safeTxGas > 0', async () => { const { safe, reverter } = await setupTests() await expect( executeContractCallWithSigners(safe, reverter, "revert", [], [user1], false, { safeTxGas: 1 }) ).to.emit(safe, "ExecutionFailure") }) it('should emit event for failed call execution if gasPrice > 0', async () => { const { safe, reverter } = await setupTests() // Fund refund await user1.sendTransaction({ to: safe.address, value: 10000000 }) await expect( executeContractCallWithSigners(safe, reverter, "revert", [], [user1], false, { gasPrice: 1 }) ).to.emit(safe, "ExecutionFailure") }) it('should revert for failed call execution if gasPrice == 0 and safeTxGas == 0', async () => { const { safe, reverter } = await setupTests() await expect( executeContractCallWithSigners(safe, reverter, "revert", [], [user1]) ).to.revertedWith("GS013") }) it('should emit event for successful delegatecall execution', async () => { const { safe, storageSetter } = await setupTests() await expect( executeContractCallWithSigners(safe, storageSetter, "setStorage", ["0xbaddad"], [user1], true) ).to.emit(safe, "ExecutionSuccess") await expect( await hre.ethers.provider.getStorageAt(safe.address, "0x4242424242424242424242424242424242424242424242424242424242424242") ).to.be.eq("0x" + "baddad".padEnd(64, "0")) await expect( await hre.ethers.provider.getStorageAt(storageSetter.address, "0x4242424242424242424242424242424242424242424242424242424242424242") ).to.be.eq("0x" + "".padEnd(64, "0")) }) it('should emit event for failed delegatecall execution if safeTxGas > 0', async () => { const { safe, reverter } = await setupTests() const txHash = calculateSafeTransactionHash(safe, buildContractCall(reverter, "revert", [], await safe.nonce(), true, { safeTxGas: 1 }), await chainId()) await expect( executeContractCallWithSigners(safe, reverter, "revert", [], [user1], true, { safeTxGas: 1 }) ).to.emit(safe, "ExecutionFailure").withArgs(txHash, 0) }) it('should emit event for failed delegatecall execution if gasPrice > 0', async () => { const { safe, reverter } = await setupTests() await user1.sendTransaction({ to: safe.address, value: 10000000 }) await expect( executeContractCallWithSigners(safe, reverter, "revert", [], [user1], true, { gasPrice: 1 }) ).to.emit(safe, "ExecutionFailure") }) it('should emit event for failed delegatecall execution if gasPrice == 0 and safeTxGas == 0', async () => { const { safe, reverter } = await setupTests() await expect( executeContractCallWithSigners(safe, reverter, "revert", [], [user1], true) ).to.revertedWith("GS013") }) it('should revert on unknown operation', async () => { const { safe } = await setupTests() const tx = buildSafeTransaction({ to: safe.address, nonce: await safe.nonce(), operation: 2 }) await expect( executeTx(safe, tx, [await safeApproveHash(user1, safe, tx, true)]) ).to.be.reverted }) it('should emit payment in success event', async () => { const { safe } = await setupTests() const tx = buildSafeTransaction({ to: user1.address, nonce: await safe.nonce(), operation: 0, gasPrice: 1, safeTxGas: 100000, refundReceiver: user2.address }) await user1.sendTransaction({ to: safe.address, value: parseEther("1") }) const userBalance = await hre.ethers.provider.getBalance(user2.address) await expect(await hre.ethers.provider.getBalance(safe.address)).to.be.deep.eq(parseEther("1")) let executedTx: any; await expect( executeTx(safe, tx, [await safeApproveHash(user1, safe, tx, true)]).then((tx) => { executedTx = tx; return tx }) ).to.emit(safe, "ExecutionSuccess") const receipt = await hre.ethers.provider.getTransactionReceipt(executedTx!!.hash) const logIndex = receipt.logs.length - 1 const successEvent = safe.interface.decodeEventLog("ExecutionSuccess", receipt.logs[logIndex].data, receipt.logs[logIndex].topics) expect(successEvent.txHash).to.be.eq(calculateSafeTransactionHash(safe, tx, await chainId())) // Gas costs are around 3000, so even if we specified a safeTxGas from 100000 we should not use more expect(successEvent.payment.toNumber()).to.be.lte(5000) await expect(await hre.ethers.provider.getBalance(user2.address)).to.be.deep.eq(userBalance.add(successEvent.payment)) }) it('should emit payment in failure event', async () => { const { safe, storageSetter } = await setupTests() const data = storageSetter.interface.encodeFunctionData("setStorage", [0xbaddad]) const tx = buildSafeTransaction({ to: storageSetter.address, data, nonce: await safe.nonce(), operation: 0, gasPrice: 1, safeTxGas: 3000, refundReceiver: user2.address }) await user1.sendTransaction({ to: safe.address, value: parseEther("1") }) const userBalance = await hre.ethers.provider.getBalance(user2.address) await expect(await hre.ethers.provider.getBalance(safe.address)).to.be.deep.eq(parseEther("1")) let executedTx: any; await expect( executeTx(safe, tx, [await safeApproveHash(user1, safe, tx, true)]).then((tx) => { executedTx = tx; return tx }) ).to.emit(safe, "ExecutionFailure") const receipt = await hre.ethers.provider.getTransactionReceipt(executedTx!!.hash) const logIndex = receipt.logs.length - 1 const successEvent = safe.interface.decodeEventLog("ExecutionFailure", receipt.logs[logIndex].data, receipt.logs[logIndex].topics) expect(successEvent.txHash).to.be.eq(calculateSafeTransactionHash(safe, tx, await chainId())) // FIXME: When running out of gas the gas used is slightly higher than the safeTxGas and the user has to overpay expect(successEvent.payment.toNumber()).to.be.lte(10000) await expect(await hre.ethers.provider.getBalance(user2.address)).to.be.deep.eq(userBalance.add(successEvent.payment)) }) it('should be possible to manually increase gas', async () => { const { safe } = await setupTests() const gasUserSource = ` contract GasUser { uint256[] public data; constructor() payable {} function nested(uint256 level, uint256 count) external { if (level == 0) { for (uint256 i = 0; i < count; i++) { data.push(i); } return; } this.nested(level - 1, count); } function useGas(uint256 count) public { this.nested(6, count); this.nested(8, count); } }` const gasUser = await deployContract(user1, gasUserSource); const to = gasUser.address const data = gasUser.interface.encodeFunctionData("useGas", [80]) const safeTxGas = 10000 const tx = buildSafeTransaction({ to, data, safeTxGas, nonce: await safe.nonce() }) await expect( executeTx(safe, tx, [await safeApproveHash(user1, safe, tx, true)], { gasLimit: 170000 }), "Safe transaction should fail with low gasLimit" ).to.emit(safe, "ExecutionFailure") await expect( executeTx(safe, tx, [await safeApproveHash(user1, safe, tx, true)], { gasLimit: 6000000 }), "Safe transaction should succeed with high gasLimit" ).to.emit(safe, "ExecutionSuccess") // This should only work if the gasPrice is 0 tx.gasPrice = 1 await user1.sendTransaction({ to: safe.address, value: parseEther("1") }) await expect( executeTx(safe, tx, [await safeApproveHash(user1, safe, tx, true)], { gasLimit: 6000000 }), "Safe transaction should fail with gasPrice 1 and high gasLimit" ).to.emit(safe, "ExecutionFailure") }) }) })