Skip to content

Commit

Permalink
MulsitsendUnwrapper tests
Browse files Browse the repository at this point in the history
  • Loading branch information
auryn-macmillan committed Jul 22, 2024
1 parent bc6f59c commit be8597f
Show file tree
Hide file tree
Showing 8 changed files with 231 additions and 30 deletions.
2 changes: 1 addition & 1 deletion contracts/Unwrappers/MultisendUnwrapper.sol
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ contract MultisendUnwrapper is ITransactionUnwrapper {
uint256 size = uint256(bytes32(data[offset:]));
offset += 32;

result[i].data = bytes(data[offset:size]);
result[i].data = bytes(data[offset:offset + size]);

offset += size;

Expand Down
69 changes: 69 additions & 0 deletions contracts/test/MultiSend.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity >=0.8.17 <0.9.0;

/// @title Multi Send - Allows to batch multiple transactions into one.
/// @author Nick Dodson - <nick.dodson@consensys.net>
/// @author Gonçalo Sá - <goncalo.sa@consensys.net>
/// @author Stefan George - <stefan@gnosis.io>
/// @author Richard Meissner - <richard@gnosis.io>
contract MultiSend {
address private immutable multisendSingleton;

constructor() {
multisendSingleton = address(this);
}

/// @dev Sends multiple transactions and reverts all if one fails.
/// @param transactions Encoded transactions. Each transaction is encoded as a packed bytes of
/// operation as a uint8 with 0 for a call or 1 for a delegatecall (=> 1 byte),
/// to as a address (=> 20 bytes),
/// value as a uint256 (=> 32 bytes),
/// data length as a uint256 (=> 32 bytes),
/// data as bytes.
/// see abi.encodePacked for more information on packed encoding
/// @notice This method is payable as delegatecalls keep the msg.value from the previous call
/// If the calling method (e.g. execTransaction) received ETH this would revert otherwise
function multiSend(bytes memory transactions) public payable {
require(
address(this) != multisendSingleton,
"MultiSend should only be called via delegatecall"
);
// solhint-disable-next-line no-inline-assembly
assembly {
let length := mload(transactions)
let i := 0x20
for {
// Pre block is not used in "while mode"
} lt(i, length) {
// Post block is not used in "while mode"
} {
// First byte of the data is the operation.
// We shift by 248 bits (256 - 8 [operation byte]) it right since mload will always load 32 bytes (a word).
// This will also zero out unused data.
let operation := shr(0xf8, mload(add(transactions, i)))
// We offset the load address by 1 byte (operation byte)
// We shift it right by 96 bits (256 - 160 [20 address bytes]) to right-align the data and zero out unused data.
let to := shr(0x60, mload(add(transactions, add(i, 0x01))))
// We offset the load address by 21 byte (operation byte + 20 address bytes)
let value := mload(add(transactions, add(i, 0x15)))
// We offset the load address by 53 byte (operation byte + 20 address bytes + 32 value bytes)
let dataLength := mload(add(transactions, add(i, 0x35)))
// We offset the load address by 85 byte (operation byte + 20 address bytes + 32 value bytes + 32 data length bytes)
let data := add(transactions, add(i, 0x55))
let success := 0
switch operation
case 0 {
success := call(gas(), to, value, data, dataLength, 0, 0)
}
case 1 {
success := delegatecall(gas(), to, data, dataLength, 0, 0)
}
if eq(success, 0) {
revert(0, 0)
}
// Next entry starts at 85 byte + data length
i := add(i, add(0x55, dataLength))
}
}
}
}
4 changes: 4 additions & 0 deletions deploy/02_test_dependencies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ const deploy: DeployFunction = async function (hre: HardhatRuntimeEnvironment) {
from: deployerAddress,
})

const multisendContract = await deploy("MultiSend", {
from: deployerAddress,
})

// Make the MockOSXDAO the owner of the button
const buttonContract = await ethers.getContractAt("Button", buttonDeployment.address, deployer)
const currentOwner = await buttonContract.owner()
Expand Down
6 changes: 6 additions & 0 deletions deploy/03_proxy_module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,12 @@ const deploy: DeployFunction = async function ({
const tx = await mockOSXDAOContract.grantExecutePermission(OSXAdapterProxyAddress)
tx.wait()
}

// Set Multisend unwrapper
const multisend = await deployments.get("MultiSend")
const multisendUnwrapperDeployment = await deployments.get("MultisendUnwrapper")
const OSXAdapterProxy = await ethers.getContractAt("OSXAdapter", OSXAdapterProxyAddress, deployer)
await OSXAdapterProxy.setTransactionUnwrapper(multisend.address, multisendUnwrapperDeployment.address)
}

deploy.tags = ["moduleProxy"]
Expand Down
2 changes: 1 addition & 1 deletion hardhat.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ const config: HardhatUserConfig = {
},
namedAccounts: {
deployer: 0,
dependenciesDeployer: 1,
user: 1,
tester: 2,
},
gasReporter: {
Expand Down
114 changes: 90 additions & 24 deletions test/00_OSXAdapter.spec.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,90 @@
import { expect } from "chai"
import { ethers, deployments, getNamedAccounts } from "hardhat"
import { encodeMultisendPayload } from "./utils"

const setup = async () => {
await deployments.fixture(["moduleProxy"])
const { deployer, tester } = await getNamedAccounts()
const buttonDeployment = await deployments.get("Button")
const OSXAdapterProxyDeployment = await deployments.get("OSXAdapterProxy")
const multisendDeployment = await deployments.get("MultiSend")
const multisendUnwrapperDeployment = await deployments.get("MultisendUnwrapper")
const buttonContract = await ethers.getContractAt("Button", buttonDeployment.address)
const OSXAdapterProxyContract = await ethers.getContractAt("OSXAdapter", OSXAdapterProxyDeployment.address)
return { buttonContract, OSXAdapterProxyContract, deployer, tester }
const multisend = await ethers.getContractAt("MultiSend", multisendDeployment.address)

const data = buttonContract.interface.encodeFunctionData("pushButton")
const txData = {
to: await buttonContract.getAddress(),
value: 0,
data: data,
operation: 0,
}

return {
buttonContract,
OSXAdapterProxyContract,
multisendDeployment,
multisend,
multisendUnwrapperDeployment,
deployer,
tester,
txData,
}
}

describe("OSXAdapter", function () {
describe("constructor / setup", function () {
it("Should set owner, avatar, and target correctly")
it("Should set owner, avatar, and target correctly", async function () {
const factory = await ethers.getContractFactory("OSXAdapter")
const { deployer: owner, user: avatar, tester: target } = await getNamedAccounts()
const oSXAdapter = await factory.deploy(owner, avatar, target)
expect(await oSXAdapter.owner()).to.equal(owner)
expect(await oSXAdapter.avatar()).to.equal(avatar)
const targetFunction = oSXAdapter.getFunction("target")
expect(await targetFunction.call(oSXAdapter)).to.equal(target)
})
})

describe("exec()", function () {
it("Should revert if called by an account that is not enabled as a module")
it("Should revert if included calls fail")
it("Should return true if all included calls execute successfully")
it("Should revert if called by an account that is not enabled as a module", async function () {
const { OSXAdapterProxyContract, tester, txData } = await setup()
const testSigner = await ethers.getSigner(tester)
expect(
await OSXAdapterProxyContract.connect(testSigner).execTransactionFromModule(
txData.to,
txData.value,
txData.data,
txData.operation,
),
)
.to.be.revertedWithCustomError(OSXAdapterProxyContract, "NotAuthorized")
.withArgs(tester)
})
it("Should revert if included calls fail", async function () {
const { OSXAdapterProxyContract, multisend, tester, txData } = await setup()
const testSigner = await ethers.getSigner(tester)
const multiSend = multisend.getFunction("multiSend")
const multisendTx = (
await multiSend.populateTransaction(encodeMultisendPayload([txData, { ...txData, data: "0xbaddda7a" }]))
).data as string
await expect(
OSXAdapterProxyContract.connect(testSigner).execTransactionFromModule(
await multisend.getAddress(),
txData.value,
multisendTx,
1,
),
).to.be.reverted
})
it("Should return true if all included calls execute successfully", async function () {
const { OSXAdapterProxyContract, deployer, txData } = await setup()
const func = OSXAdapterProxyContract.getFunction("execTransactionFromModule")
const result = await func.staticCall(txData.to, txData.value, txData.data, txData.operation)
expect(result).to.be.true
})
it("Should trigger OSx to make external calls", async function () {
const { buttonContract, OSXAdapterProxyContract, deployer } = await setup()
const { buttonContract, OSXAdapterProxyContract, deployer, multisend, txData } = await setup()
expect(await buttonContract.pushes()).to.equal(0)

expect(await OSXAdapterProxyContract.enableModule(deployer))
Expand All @@ -31,29 +94,32 @@ describe("OSXAdapter", function () {
10,
)

const data = buttonContract.interface.encodeFunctionData("pushButton")
// const multisendTx = encodeMultisendPayload([txData, txData])
const multiSend = multisend.getFunction("multiSend")
const multisendTx = (await multiSend.populateTransaction(encodeMultisendPayload([txData, txData]))).data as string
expect(
await OSXAdapterProxyContract.execTransactionFromModule(multisend.getAddress(), txData.value, multisendTx, 1),
)

const txData = {
to: await buttonContract.getAddress(),
value: 0,
data: data,
operation: 0,
}
expect(await buttonContract.pushes()).to.equal(2)
})
it("Should emit `ExecutionFromModuleSuccess` with correct args", async function () {
const { buttonContract, OSXAdapterProxyContract, deployer, txData } = await setup()
expect(await buttonContract.pushes()).to.equal(0)

const tx = OSXAdapterProxyContract.interface.encodeFunctionData("execTransactionFromModule", [
txData.to,
txData.value,
txData.data,
txData.operation,
])
expect(await OSXAdapterProxyContract.enableModule(deployer))

const result = await (
await OSXAdapterProxyContract.execTransactionFromModule(txData.to, txData.value, txData.data, txData.operation)
).wait()
const enabledModules = await OSXAdapterProxyContract.getModulesPaginated(
"0x0000000000000000000000000000000000000001",
10,
)

expect(await buttonContract.pushes()).to.equal(1)
expect(
await OSXAdapterProxyContract.execTransactionFromModule(txData.to, txData.value, txData.data, txData.operation),
)
.to.emit(OSXAdapterProxyContract, "ExecutionFromModuleSuccess")
.withArgs(deployer)
})
it("Should emit `ExecutionFromModuleSuccess` with correct args")
})

describe("execAndReturnData()", function () {
Expand Down
42 changes: 38 additions & 4 deletions test/01_MultisendUnwrapper.spec.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import { expect } from "chai"
import { ethers, deployments, getNamedAccounts } from "hardhat"
import { encodeMultisendPayload } from "./utils"

const setup = async () => {
await deployments.fixture(["moduleProxy"])
const { deployer, tester } = await getNamedAccounts()
const multisendDeployement = await deployments.get("MultiSend")
const multisendUnwrapperDeployment = await deployments.get("MultisendUnwrapper")
const multisend = await ethers.getContractAt("MultiSend", multisendDeployement.address)
const multisendUnwrapper = await ethers.getContractAt("MultisendUnwrapper", multisendUnwrapperDeployment.address)
return { multisendUnwrapper, multisendUnwrapperDeployment, deployer, tester }
return { multisend, multisendDeployement, multisendUnwrapper, multisendUnwrapperDeployment, deployer, tester }
}

describe("MultisendUnwrapper", function () {
Expand All @@ -18,8 +21,39 @@ describe("MultisendUnwrapper", function () {
})

describe("unwrap()", function () {
it("Should revert revert with `UnsupportedMode` if value is non-zero")
it("Shoudl revert with UnsupportedMode if operation is not `DelegateCall`")
it("Should correctly unwrap the multisend call")
it("Should revert revert with `UnsupportedMode` if value is non-zero", async function () {
const { multisendUnwrapper } = await setup()
await expect(multisendUnwrapper.unwrap(ethers.ZeroAddress, 1, "0x", 1)).to.be.revertedWithCustomError(
multisendUnwrapper,
"UnsupportedMode",
)
})
it("Shoudl revert with UnsupportedMode if operation is not `DelegateCall`", async function () {
const { multisendUnwrapper } = await setup()
await expect(multisendUnwrapper.unwrap(ethers.ZeroAddress, 0, "0x", 0)).to.be.revertedWithCustomError(
multisendUnwrapper,
"UnsupportedMode",
)
})
it("Should correctly unwrap the multisend call", async function () {
const { multisendUnwrapper, multisend } = await setup()

const txData = {
to: "0x1111111111111111111111111111111111111111",
value: 0,
data: "0x1337C0D3",
operation: 0,
}

const multiSend = multisend.getFunction("multiSend")
const multisendTx = (await multiSend.populateTransaction(encodeMultisendPayload([txData, txData]))).data as string

const tx = await multisendUnwrapper.unwrap(await multisend.getAddress(), 0, multisendTx, 1)
const expectedResult = [
[txData.operation, txData.to, txData.value, txData.data],
[txData.operation, txData.to, txData.value, txData.data],
]
expect(tx).to.deep.equal(expectedResult)
})
})
})
22 changes: 22 additions & 0 deletions test/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { BigNumberish, solidityPacked } from "ethers"

export interface MetaTransaction {
to: string
value: BigNumberish
data: string
operation: number
}

export const encodeMultisendPayload = (txs: MetaTransaction[]): string => {
return (
"0x" +
txs
.map((tx) =>
solidityPacked(
["uint8", "address", "uint256", "uint256", "bytes"],
[tx.operation, tx.to, tx.value, (tx.data.length - 2) / 2, tx.data],
).slice(2),
)
.join("")
)
}

0 comments on commit be8597f

Please sign in to comment.