Skip to content

Commit 9e6f927

Browse files
authored
Closes #227: Allow multisend when msg.value > 0; Add call only multisend (#286)
1 parent b4ac134 commit 9e6f927

File tree

6 files changed

+251
-0
lines changed

6 files changed

+251
-0
lines changed

contracts/libraries/MultiSend.sol

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,11 @@ contract MultiSend {
2525
/// data length as a uint256 (=> 32 bytes),
2626
/// data as bytes.
2727
/// see abi.encodePacked for more information on packed encoding
28+
/// @notice This method is payable as delegatecalls keep the msg.value from the previous call
29+
/// If the calling method (e.g. execTransaction) received ETH this would revert otherwise
2830
function multiSend(bytes memory transactions)
2931
public
32+
payable
3033
{
3134
require(guard != GUARD_VALUE, "MultiSend should only be called via delegatecall");
3235
// solium-disable-next-line security/no-inline-assembly
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
// SPDX-License-Identifier: LGPL-3.0-only
2+
pragma solidity >=0.7.0 <0.9.0;
3+
4+
5+
/// @title Multi Send Call Only - Allows to batch multiple transactions into one, but only calls
6+
/// @author Stefan George - <stefan@gnosis.io>
7+
/// @author Richard Meissner - <richard@gnosis.io>
8+
/// @notice The guard logic is not required here as this contract doesn't support nested delegate calls
9+
contract MultiSendCallOnly {
10+
11+
/// @dev Sends multiple transactions and reverts all if one fails.
12+
/// @param transactions Encoded transactions. Each transaction is encoded as a packed bytes of
13+
/// operation has to be uint8(0) in this version (=> 1 byte),
14+
/// to as a address (=> 20 bytes),
15+
/// value as a uint256 (=> 32 bytes),
16+
/// data length as a uint256 (=> 32 bytes),
17+
/// data as bytes.
18+
/// see abi.encodePacked for more information on packed encoding
19+
/// @notice The code is for most part the same as the normal MultiSend (to keep compatibility),
20+
/// but reverts if a transaction tries to use a delegatecall.
21+
/// @notice This method is payable as delegatecalls keep the msg.value from the previous call
22+
/// If the calling method (e.g. execTransaction) received ETH this would revert otherwise
23+
function multiSend(bytes memory transactions)
24+
public
25+
payable
26+
{
27+
// solium-disable-next-line security/no-inline-assembly
28+
assembly {
29+
let length := mload(transactions)
30+
let i := 0x20
31+
for { } lt(i, length) { } {
32+
// First byte of the data is the operation.
33+
// We shift by 248 bits (256 - 8 [operation byte]) it right since mload will always load 32 bytes (a word).
34+
// This will also zero out unused data.
35+
let operation := shr(0xf8, mload(add(transactions, i)))
36+
// We offset the load address by 1 byte (operation byte)
37+
// We shift it right by 96 bits (256 - 160 [20 address bytes]) to right-align the data and zero out unused data.
38+
let to := shr(0x60, mload(add(transactions, add(i, 0x01))))
39+
// We offset the load address by 21 byte (operation byte + 20 address bytes)
40+
let value := mload(add(transactions, add(i, 0x15)))
41+
// We offset the load address by 53 byte (operation byte + 20 address bytes + 32 value bytes)
42+
let dataLength := mload(add(transactions, add(i, 0x35)))
43+
// We offset the load address by 85 byte (operation byte + 20 address bytes + 32 value bytes + 32 data length bytes)
44+
let data := add(transactions, add(i, 0x55))
45+
let success := 0
46+
switch operation
47+
case 0 { success := call(gas(), to, value, data, dataLength, 0, 0) }
48+
// This version does not allow delegatecalls
49+
case 1 { revert(0, 0) }
50+
if eq(success, 0) { revert(0, 0) }
51+
// Next entry starts at 85 byte + data length
52+
i := add(i, add(0x55, dataLength))
53+
}
54+
}
55+
}
56+
}

src/deploy/deploy_libraries.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,13 @@ const deploy: DeployFunction = async function (
2121
log: true,
2222
deterministicDeployment: true,
2323
});
24+
25+
await deploy("MultiSendCallOnly", {
26+
from: deployer,
27+
args: [],
28+
log: true,
29+
deterministicDeployment: true,
30+
});
2431
};
2532

2633
deploy.tags = ['libraries']

test/libraries/MultiSend.spec.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,23 @@ describe("MultiSend", async () => {
108108
await expect(await hre.ethers.provider.getBalance(user2.address)).to.be.deep.eq(userBalance)
109109
})
110110

111+
it('can be used when ETH is sent with execution', async () => {
112+
const { safe, multiSend, storageSetter } = await setupTests()
113+
114+
const txs: MetaTransaction[] = [
115+
buildContractCall(storageSetter, "setStorage", ["0xbaddad"], 0)
116+
]
117+
const safeTx = buildMultiSendSafeTx(multiSend, txs, await safe.nonce())
118+
119+
await expect(await hre.ethers.provider.getBalance(safe.address)).to.be.deep.eq(parseEther("0"))
120+
121+
await expect(
122+
executeTx(safe, safeTx, [ await safeApproveHash(user1, safe, safeTx, true) ], { value: parseEther("1") })
123+
).to.emit(safe, "ExecutionSuccess")
124+
125+
await expect(await hre.ethers.provider.getBalance(safe.address)).to.be.deep.eq(parseEther("1"))
126+
})
127+
111128
it('can execute contract calls', async () => {
112129
const { safe, multiSend, storageSetter } = await setupTests()
113130

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
import { expect } from "chai";
2+
import hre, { deployments, waffle } from "hardhat";
3+
import "@nomiclabs/hardhat-ethers";
4+
import { deployContract, getMock, getMultiSendCallOnly, getSafeWithOwners } from "../utils/setup";
5+
import { buildContractCall, buildSafeTransaction, executeTx, MetaTransaction, safeApproveHash } from "../utils/execution";
6+
import { buildMultiSendSafeTx } from "../utils/multisend";
7+
import { parseEther } from "@ethersproject/units";
8+
9+
describe("MultiSendCallOnly", async () => {
10+
11+
const [user1, user2] = waffle.provider.getWallets();
12+
13+
const setupTests = deployments.createFixture(async ({ deployments }) => {
14+
await deployments.fixture();
15+
const setterSource = `
16+
contract StorageSetter {
17+
function setStorage(bytes3 data) public {
18+
bytes32 slot = 0x4242424242424242424242424242424242424242424242424242424242424242;
19+
// solium-disable-next-line security/no-inline-assembly
20+
assembly {
21+
sstore(slot, data)
22+
}
23+
}
24+
}`
25+
const storageSetter = await deployContract(user1, setterSource);
26+
return {
27+
safe: await getSafeWithOwners([user1.address]),
28+
multiSend: await getMultiSendCallOnly(),
29+
mock: await getMock(),
30+
storageSetter
31+
}
32+
})
33+
34+
describe("multiSend", async () => {
35+
36+
it('Should fail when using invalid operation', async () => {
37+
const { safe, multiSend } = await setupTests()
38+
39+
const txs = [buildSafeTransaction({to: user2.address, operation: 2, nonce: 0})]
40+
const safeTx = buildMultiSendSafeTx(multiSend, txs, await safe.nonce())
41+
await expect(
42+
executeTx(safe, safeTx, [ await safeApproveHash(user1, safe, safeTx, true) ])
43+
).to.revertedWith("GS013")
44+
})
45+
46+
it('Should fail when using delegatecall operation', async () => {
47+
const { safe, multiSend } = await setupTests()
48+
49+
const txs = [buildSafeTransaction({to: user2.address, operation: 1, nonce: 0})]
50+
const safeTx = buildMultiSendSafeTx(multiSend, txs, await safe.nonce())
51+
await expect(
52+
executeTx(safe, safeTx, [ await safeApproveHash(user1, safe, safeTx, true) ])
53+
).to.revertedWith("GS013")
54+
})
55+
56+
it('Can execute empty multisend', async () => {
57+
const { safe, multiSend } = await setupTests()
58+
59+
const txs: MetaTransaction[] = []
60+
const safeTx = buildMultiSendSafeTx(multiSend, txs, await safe.nonce())
61+
await expect(
62+
executeTx(safe, safeTx, [ await safeApproveHash(user1, safe, safeTx, true) ])
63+
).to.emit(safe, "ExecutionSuccess")
64+
})
65+
66+
it('Can execute single ether transfer', async () => {
67+
const { safe, multiSend } = await setupTests()
68+
await user1.sendTransaction({to: safe.address, value: parseEther("1")})
69+
const userBalance = await hre.ethers.provider.getBalance(user2.address)
70+
await expect(await hre.ethers.provider.getBalance(safe.address)).to.be.deep.eq(parseEther("1"))
71+
72+
const txs: MetaTransaction[] = [buildSafeTransaction({to: user2.address, value: parseEther("1"), nonce: 0})]
73+
const safeTx = buildMultiSendSafeTx(multiSend, txs, await safe.nonce())
74+
await expect(
75+
executeTx(safe, safeTx, [ await safeApproveHash(user1, safe, safeTx, true) ])
76+
).to.emit(safe, "ExecutionSuccess")
77+
78+
await expect(await hre.ethers.provider.getBalance(safe.address)).to.be.deep.eq(parseEther("0"))
79+
await expect(await hre.ethers.provider.getBalance(user2.address)).to.be.deep.eq(userBalance.add(parseEther("1")))
80+
})
81+
82+
it('reverts all tx if any fails', async () => {
83+
const { safe, multiSend } = await setupTests()
84+
await user1.sendTransaction({to: safe.address, value: parseEther("1")})
85+
const userBalance = await hre.ethers.provider.getBalance(user2.address)
86+
await expect(await hre.ethers.provider.getBalance(safe.address)).to.be.deep.eq(parseEther("1"))
87+
88+
const txs: MetaTransaction[] = [
89+
buildSafeTransaction({to: user2.address, value: parseEther("1"), nonce: 0}),
90+
buildSafeTransaction({to: user2.address, value: parseEther("1"), nonce: 0}),
91+
]
92+
const safeTx = buildMultiSendSafeTx(multiSend, txs, await safe.nonce(), { safeTxGas: 1 })
93+
await expect(
94+
executeTx(safe, safeTx, [ await safeApproveHash(user1, safe, safeTx, true) ])
95+
).to.emit(safe, "ExecutionFailure")
96+
97+
await expect(await hre.ethers.provider.getBalance(safe.address)).to.be.deep.eq(parseEther("1"))
98+
await expect(await hre.ethers.provider.getBalance(user2.address)).to.be.deep.eq(userBalance)
99+
})
100+
101+
it('can be used when ETH is sent with execution', async () => {
102+
const { safe, multiSend, storageSetter } = await setupTests()
103+
104+
const txs: MetaTransaction[] = [
105+
buildContractCall(storageSetter, "setStorage", ["0xbaddad"], 0)
106+
]
107+
const safeTx = buildMultiSendSafeTx(multiSend, txs, await safe.nonce())
108+
109+
await expect(await hre.ethers.provider.getBalance(safe.address)).to.be.deep.eq(parseEther("0"))
110+
111+
await expect(
112+
executeTx(safe, safeTx, [ await safeApproveHash(user1, safe, safeTx, true) ], { value: parseEther("1") })
113+
).to.emit(safe, "ExecutionSuccess")
114+
115+
await expect(await hre.ethers.provider.getBalance(safe.address)).to.be.deep.eq(parseEther("1"))
116+
})
117+
118+
it('can execute contract calls', async () => {
119+
const { safe, multiSend, storageSetter } = await setupTests()
120+
121+
const txs: MetaTransaction[] = [
122+
buildContractCall(storageSetter, "setStorage", ["0xbaddad"], 0)
123+
]
124+
const safeTx = buildMultiSendSafeTx(multiSend, txs, await safe.nonce())
125+
await expect(
126+
executeTx(safe, safeTx, [ await safeApproveHash(user1, safe, safeTx, true) ])
127+
).to.emit(safe, "ExecutionSuccess")
128+
129+
await expect(
130+
await hre.ethers.provider.getStorageAt(safe.address, "0x4242424242424242424242424242424242424242424242424242424242424242")
131+
).to.be.eq("0x" + "".padEnd(64, "0"))
132+
await expect(
133+
await hre.ethers.provider.getStorageAt(storageSetter.address, "0x4242424242424242424242424242424242424242424242424242424242424242")
134+
).to.be.eq("0x" + "baddad".padEnd(64, "0"))
135+
})
136+
137+
it('can execute combinations', async () => {
138+
const { safe, multiSend, storageSetter } = await setupTests()
139+
await user1.sendTransaction({to: safe.address, value: parseEther("1")})
140+
const userBalance = await hre.ethers.provider.getBalance(user2.address)
141+
await expect(await hre.ethers.provider.getBalance(safe.address)).to.be.deep.eq(parseEther("1"))
142+
143+
const txs: MetaTransaction[] = [
144+
buildSafeTransaction({to: user2.address, value: parseEther("1"), nonce: 0}),
145+
buildContractCall(storageSetter, "setStorage", ["0xbaddad"], 0)
146+
]
147+
const safeTx = buildMultiSendSafeTx(multiSend, txs, await safe.nonce())
148+
await expect(
149+
executeTx(safe, safeTx, [ await safeApproveHash(user1, safe, safeTx, true) ])
150+
).to.emit(safe, "ExecutionSuccess")
151+
152+
await expect(await hre.ethers.provider.getBalance(safe.address)).to.be.deep.eq(parseEther("0"))
153+
await expect(await hre.ethers.provider.getBalance(user2.address)).to.be.deep.eq(userBalance.add(parseEther("1")))
154+
await expect(
155+
await hre.ethers.provider.getStorageAt(safe.address, "0x4242424242424242424242424242424242424242424242424242424242424242")
156+
).to.be.eq("0x" + "".padEnd(64, "0"))
157+
await expect(
158+
await hre.ethers.provider.getStorageAt(storageSetter.address, "0x4242424242424242424242424242424242424242424242424242424242424242")
159+
).to.be.eq("0x" + "baddad".padEnd(64, "0"))
160+
})
161+
})
162+
})

test/utils/setup.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,12 @@ export const getMultiSend = async () => {
4444
return MultiSend.attach(MultiSendDeployment.address);
4545
}
4646

47+
export const getMultiSendCallOnly = async () => {
48+
const MultiSendDeployment = await deployments.get("MultiSendCallOnly");
49+
const MultiSend = await hre.ethers.getContractFactory("MultiSendCallOnly");
50+
return MultiSend.attach(MultiSendDeployment.address);
51+
}
52+
4753
export const getCreateCall = async () => {
4854
const CreateCallDeployment = await deployments.get("CreateCall");
4955
const CreateCall = await hre.ethers.getContractFactory("CreateCall");

0 commit comments

Comments
 (0)