-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Loading status checks…
test: hook tests (#260)
* test: dummy hook test * test: add hook tests * chore: add fixme
Showing
8 changed files
with
426 additions
and
13 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity ^0.8.28; | ||
|
||
import { Transaction, TransactionHelper } from "@matterlabs/zksync-contracts/l2/system-contracts/libraries/TransactionHelper.sol"; | ||
import { IExecutionHook } from "../interfaces/IHook.sol"; | ||
import { IModule } from "../interfaces/IModule.sol"; | ||
import { IERC165 } from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; | ||
|
||
contract TestExecutionHook is IExecutionHook { | ||
using TransactionHelper for Transaction; | ||
|
||
event ExecutionHookInstalled(address indexed account); | ||
event ExecutionHookUninstalled(address indexed account); | ||
|
||
event PreExecution(address indexed account, address indexed target); | ||
event PostExecution(address indexed account, address indexed target); | ||
|
||
mapping(address => address) public lastTarget; | ||
|
||
function onInstall(bytes calldata data) external { | ||
bool shouldRevert = abi.decode(data, (bool)); | ||
if (shouldRevert) { | ||
revert("Install hook failed"); | ||
} | ||
emit ExecutionHookInstalled(msg.sender); | ||
} | ||
|
||
function onUninstall(bytes calldata data) external { | ||
bool shouldRevert = abi.decode(data, (bool)); | ||
if (shouldRevert) { | ||
revert("Uninstall hook failed"); | ||
} | ||
emit ExecutionHookUninstalled(msg.sender); | ||
} | ||
|
||
function supportsInterface(bytes4 interfaceId) external pure override returns (bool) { | ||
return | ||
interfaceId == type(IExecutionHook).interfaceId || | ||
interfaceId == type(IModule).interfaceId || | ||
interfaceId == type(IERC165).interfaceId; | ||
} | ||
|
||
function preExecutionHook(Transaction calldata transaction) external returns (bytes memory context) { | ||
// arbitrary revert condition | ||
if (transaction.to == 0) { | ||
revert("PreExecution hook failed"); | ||
} | ||
|
||
// store some data in transient storage | ||
uint256 slot = uint256(uint160(msg.sender)); | ||
uint256 storedTarget = transaction.to; | ||
assembly { | ||
tstore(slot, storedTarget) | ||
} | ||
|
||
// store some data in regular storage | ||
lastTarget[msg.sender] = address(uint160(transaction.to)); | ||
|
||
// emit event | ||
emit PreExecution(msg.sender, address(uint160(transaction.to))); | ||
|
||
// pass some data via context | ||
return abi.encode(transaction); | ||
} | ||
|
||
function postExecutionHook(bytes calldata context) external { | ||
// decode context data | ||
Transaction memory transaction = abi.decode(context, (Transaction)); | ||
|
||
// arbitrary revert condition | ||
if (transaction.to == uint256(uint160(msg.sender))) { | ||
revert("PostExecution hook failed"); | ||
} | ||
|
||
// load data from transient storage | ||
uint256 storedTarget; | ||
uint256 slot = uint256(uint160(msg.sender)); | ||
assembly { | ||
storedTarget := tload(slot) | ||
} | ||
|
||
require(storedTarget != 0, "No data in transient storage"); | ||
require(transaction.to == storedTarget, "Targets do not match (tload)"); | ||
require(transaction.to == uint256(uint160(lastTarget[msg.sender])), "Targets do not match (sload)"); | ||
|
||
// emit event | ||
emit PostExecution(msg.sender, address(uint160(transaction.to))); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity ^0.8.28; | ||
|
||
import { Transaction, TransactionHelper } from "@matterlabs/zksync-contracts/l2/system-contracts/libraries/TransactionHelper.sol"; | ||
import { IValidationHook } from "../interfaces/IHook.sol"; | ||
import { IModule } from "../interfaces/IModule.sol"; | ||
import { IERC165 } from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; | ||
|
||
contract TestValidationHook is IValidationHook { | ||
using TransactionHelper for Transaction; | ||
|
||
event ValidationHookInstalled(address indexed account); | ||
event ValidationHookUninstalled(address indexed account); | ||
event ValidationHookTriggered(address indexed account, address indexed target); | ||
|
||
mapping(address => address) public lastTarget; | ||
|
||
function onInstall(bytes calldata data) external { | ||
bool shouldRevert = abi.decode(data, (bool)); | ||
if (shouldRevert) { | ||
revert("Install hook failed"); | ||
} | ||
emit ValidationHookInstalled(msg.sender); | ||
} | ||
|
||
function onUninstall(bytes calldata data) external { | ||
bool shouldRevert = abi.decode(data, (bool)); | ||
if (shouldRevert) { | ||
revert("Uninstall hook failed"); | ||
} | ||
emit ValidationHookUninstalled(msg.sender); | ||
} | ||
|
||
function supportsInterface(bytes4 interfaceId) external pure override returns (bool) { | ||
return | ||
interfaceId == type(IValidationHook).interfaceId || | ||
interfaceId == type(IModule).interfaceId || | ||
interfaceId == type(IERC165).interfaceId; | ||
} | ||
|
||
// FIXME: if a validation hook were to always revert, the account would be bricked | ||
function validationHook(bytes32 signedHash, Transaction calldata transaction) external { | ||
if (transaction.data.length == 0) { | ||
revert("Empty calldata not allowed"); | ||
} | ||
|
||
address target = address(uint160(transaction.to)); | ||
|
||
// emit event | ||
emit ValidationHookTriggered(msg.sender, target); | ||
|
||
// store some data in storage | ||
lastTarget[msg.sender] = target; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,260 @@ | ||
import { assert, expect } from "chai"; | ||
import { ethers, parseEther, randomBytes } from "ethers"; | ||
import { Wallet, ZeroAddress } from "ethers"; | ||
import { it } from "mocha"; | ||
import { SmartAccount, utils } from "zksync-ethers"; | ||
import { create2 } from "./utils"; | ||
|
||
import { SsoAccount__factory, TestExecutionHook__factory, TestValidationHook__factory } from "../typechain-types"; | ||
import type { TestExecutionHook, TestValidationHook, SsoAccount } from "../typechain-types"; | ||
import { ContractFixtures, getProvider } from "./utils"; | ||
|
||
const ssoAccountAbi = SsoAccount__factory.createInterface(); | ||
|
||
describe("Hook tests", function () { | ||
const fixtures = new ContractFixtures(); | ||
const provider = getProvider(); | ||
let proxyAccountAddress: string; | ||
let ssoAccount: SsoAccount; | ||
let executionHook: TestExecutionHook; | ||
let validationHook: TestValidationHook; | ||
const abi = new ethers.AbiCoder(); | ||
let smartAccount: SmartAccount; | ||
|
||
async function aaTxTemplate() { | ||
return { | ||
type: 113, | ||
from: proxyAccountAddress, | ||
data: "0x", | ||
value: 0, | ||
chainId: (await provider.getNetwork()).chainId, | ||
nonce: await provider.getTransactionCount(proxyAccountAddress), | ||
gasPrice: await provider.getGasPrice(), | ||
customData: { gasPerPubdata: utils.DEFAULT_GAS_PER_PUBDATA_LIMIT }, | ||
gasLimit: 500_000n, | ||
}; | ||
} | ||
|
||
it("should deploy proxy account and hooks", async () => { | ||
const accountImplContract = await fixtures.getAccountImplContract(); | ||
assert(accountImplContract != null, "No account impl deployed"); | ||
const aaFactoryContract = await fixtures.getAaFactory(); | ||
|
||
const randomSalt = randomBytes(32); | ||
const deployTx = await aaFactoryContract.deployProxySsoAccount( | ||
randomSalt, | ||
"id" + randomBytes(32).toString(), | ||
[], | ||
[fixtures.wallet.address], | ||
); | ||
const deployTxReceipt = await deployTx.wait(); | ||
proxyAccountAddress = deployTxReceipt!.contractAddress!; | ||
ssoAccount = SsoAccount__factory.connect(proxyAccountAddress, fixtures.wallet); | ||
smartAccount = new SmartAccount({ | ||
address: proxyAccountAddress, | ||
secret: fixtures.wallet.privateKey, | ||
}, provider); | ||
|
||
const validationHookContract = await create2("TestValidationHook", fixtures.wallet, randomSalt); | ||
validationHook = TestValidationHook__factory.connect(await validationHookContract.getAddress(), fixtures.wallet); | ||
|
||
const executionHookContract = await create2("TestExecutionHook", fixtures.wallet, randomSalt); | ||
executionHook = TestExecutionHook__factory.connect(await executionHookContract.getAddress(), fixtures.wallet); | ||
|
||
const fundTx = await fixtures.wallet.sendTransaction({ value: parseEther("0.2"), to: proxyAccountAddress }); | ||
await fundTx.wait(); | ||
}); | ||
|
||
describe("Validation hook tests", function () { | ||
it("should revert on install", async () => { | ||
const aaTx = { | ||
...await aaTxTemplate(), | ||
to: proxyAccountAddress, | ||
data: ssoAccountAbi.encodeFunctionData("addHook", [await validationHook.getAddress(), true, abi.encode(["bool"], [true])]), | ||
}; | ||
|
||
const signedTx = await smartAccount.signTransaction(aaTx); | ||
await expect(provider.broadcastTransaction(signedTx)).to.be.reverted; | ||
}); | ||
|
||
it("should install hook", async () => { | ||
const aaTx = { | ||
...await aaTxTemplate(), | ||
to: proxyAccountAddress, | ||
data: ssoAccountAbi.encodeFunctionData("addHook", [await validationHook.getAddress(), true, abi.encode(["bool"], [false])]), | ||
}; | ||
|
||
const signedTx = await smartAccount.signTransaction(aaTx); | ||
await expect(provider.broadcastTransaction(signedTx)) | ||
.to.emit(validationHook, "ValidationHookInstalled") | ||
.and.to.emit(ssoAccount, "HookAdded"); | ||
expect(await ssoAccount.isHook(await validationHook.getAddress())).to.be.true; | ||
}); | ||
|
||
it("should fail installing existing hook", async () => { | ||
const aaTx = { | ||
...await aaTxTemplate(), | ||
to: proxyAccountAddress, | ||
data: ssoAccountAbi.encodeFunctionData("addHook", [await validationHook.getAddress(), true, abi.encode(["bool"], [false])]), | ||
}; | ||
|
||
const signedTx = await smartAccount.signTransaction(aaTx); | ||
await expect(provider.broadcastTransaction(signedTx)).to.be.reverted; | ||
}); | ||
|
||
it("should revert while sending a transaction", async () => { | ||
const aaTx = { | ||
...await aaTxTemplate(), | ||
to: Wallet.createRandom().address, | ||
}; | ||
|
||
const signedTx = await smartAccount.signTransaction(aaTx); | ||
await expect(provider.broadcastTransaction(signedTx)).to.be.reverted; | ||
}); | ||
|
||
it("should send transaction and emit event", async () => { | ||
const aaTx = { | ||
...await aaTxTemplate(), | ||
to: Wallet.createRandom().address, | ||
data: "0x1234", | ||
}; | ||
|
||
const signedTx = await smartAccount.signTransaction(aaTx); | ||
const tx = await provider.broadcastTransaction(signedTx); | ||
await expect(tx).to.emit(validationHook, "ValidationHookTriggered"); | ||
expect(await validationHook.lastTarget(proxyAccountAddress)).to.equal(tx.to); | ||
}); | ||
|
||
it("should revert on uninstall", async () => { | ||
const aaTx = { | ||
...await aaTxTemplate(), | ||
to: proxyAccountAddress, | ||
data: ssoAccountAbi.encodeFunctionData("removeHook", [await validationHook.getAddress(), true, abi.encode(["bool"], [true])]), | ||
}; | ||
|
||
const signedTx = await smartAccount.signTransaction(aaTx); | ||
await expect(provider.broadcastTransaction(signedTx)).to.be.reverted; | ||
}); | ||
|
||
it("should uninstall hook", async () => { | ||
const aaTx = { | ||
...await aaTxTemplate(), | ||
to: proxyAccountAddress, | ||
data: ssoAccountAbi.encodeFunctionData("removeHook", [await validationHook.getAddress(), true, abi.encode(["bool"], [false])]), | ||
}; | ||
|
||
const signedTx = await smartAccount.signTransaction(aaTx); | ||
await expect(provider.broadcastTransaction(signedTx)) | ||
.to.emit(validationHook, "ValidationHookUninstalled") | ||
.and.to.emit(ssoAccount, "HookRemoved"); | ||
expect(await ssoAccount.isHook(await validationHook.getAddress())).to.be.false; | ||
}); | ||
|
||
it("should fail uninstalling already uninstalled hook", async () => { | ||
const aaTx = { | ||
...await aaTxTemplate(), | ||
to: proxyAccountAddress, | ||
data: ssoAccountAbi.encodeFunctionData("removeHook", [await validationHook.getAddress(), true, abi.encode(["bool"], [false])]), | ||
}; | ||
|
||
const signedTx = await smartAccount.signTransaction(aaTx); | ||
await expect(provider.broadcastTransaction(signedTx)).to.be.reverted; | ||
}); | ||
}); | ||
|
||
describe("Execution hook tests", function () { | ||
it("should revert on install", async () => { | ||
const aaTx = { | ||
...await aaTxTemplate(), | ||
to: proxyAccountAddress, | ||
data: ssoAccountAbi.encodeFunctionData("addHook", [await executionHook.getAddress(), false, abi.encode(["bool"], [true])]), | ||
}; | ||
|
||
const signedTx = await smartAccount.signTransaction(aaTx); | ||
await expect(provider.broadcastTransaction(signedTx)).to.be.reverted; | ||
}); | ||
|
||
it("should install hook", async () => { | ||
const aaTx = { | ||
...await aaTxTemplate(), | ||
to: proxyAccountAddress, | ||
data: ssoAccountAbi.encodeFunctionData("addHook", [await executionHook.getAddress(), false, abi.encode(["bool"], [false])]), | ||
}; | ||
|
||
const signedTx = await smartAccount.signTransaction(aaTx); | ||
await expect(provider.broadcastTransaction(signedTx)) | ||
.to.emit(executionHook, "ExecutionHookInstalled") | ||
.and.to.emit(ssoAccount, "HookAdded"); | ||
expect(await ssoAccount.isHook(await executionHook.getAddress())).to.be.true; | ||
}); | ||
|
||
it("should revert while sending a transaction", async () => { | ||
// pre execution reverts | ||
let aaTx = { | ||
...await aaTxTemplate(), | ||
to: ZeroAddress | ||
}; | ||
|
||
let signedTx = await smartAccount.signTransaction(aaTx); | ||
await expect(provider.broadcastTransaction(signedTx)).to.be.reverted; | ||
|
||
// post execution reverts | ||
aaTx = { | ||
...await aaTxTemplate(), | ||
to: proxyAccountAddress | ||
}; | ||
|
||
signedTx = await smartAccount.signTransaction(aaTx); | ||
await expect(provider.broadcastTransaction(signedTx)).to.be.reverted; | ||
}); | ||
|
||
it("should send transaction and emit event", async () => { | ||
const aaTx = { | ||
...await aaTxTemplate(), | ||
to: Wallet.createRandom().address, | ||
}; | ||
|
||
const signedTx = await smartAccount.signTransaction(aaTx); | ||
const tx = await provider.broadcastTransaction(signedTx); | ||
await expect(tx) | ||
.to.emit(executionHook, "PreExecution") | ||
.and.to.emit(executionHook, "PostExecution"); | ||
expect(await executionHook.lastTarget(proxyAccountAddress)).to.equal(tx.to); | ||
}); | ||
|
||
it("should revert on uninstall", async () => { | ||
const aaTx = { | ||
...await aaTxTemplate(), | ||
to: proxyAccountAddress, | ||
data: ssoAccountAbi.encodeFunctionData("removeHook", [await executionHook.getAddress(), false, abi.encode(["bool"], [true])]), | ||
}; | ||
|
||
const signedTx = await smartAccount.signTransaction(aaTx); | ||
await expect(provider.broadcastTransaction(signedTx)).to.be.reverted; | ||
}); | ||
|
||
it("should unlink hook", async () => { | ||
const aaTx = { | ||
...await aaTxTemplate(), | ||
to: proxyAccountAddress, | ||
data: ssoAccountAbi.encodeFunctionData("unlinkHook", [await executionHook.getAddress(), false, abi.encode(["bool"], [false])]), | ||
}; | ||
|
||
const signedTx = await smartAccount.signTransaction(aaTx); | ||
await expect(provider.broadcastTransaction(signedTx)) | ||
.and.to.emit(ssoAccount, "HookRemoved"); | ||
expect(await ssoAccount.isHook(await executionHook.getAddress())).to.be.false; | ||
}); | ||
|
||
it("should fail unlinking already unlinked hook", async () => { | ||
const aaTx = { | ||
...await aaTxTemplate(), | ||
to: proxyAccountAddress, | ||
data: ssoAccountAbi.encodeFunctionData("unlinkHook", [await executionHook.getAddress(), false, abi.encode(["bool"], [false])]), | ||
}; | ||
|
||
const signedTx = await smartAccount.signTransaction(aaTx); | ||
await expect(provider.broadcastTransaction(signedTx)).to.be.reverted; | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters