Skip to content

Commit

Permalink
docs: add multicaller example (#241)
Browse files Browse the repository at this point in the history
  • Loading branch information
daejunpark authored Dec 23, 2023
1 parent 33dcdda commit 70ddda2
Show file tree
Hide file tree
Showing 5 changed files with 385 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,6 @@
[submodule "tests/lib/solady"]
path = tests/lib/solady
url = https://github.com/Vectorized/solady
[submodule "tests/lib/multicaller"]
path = tests/lib/multicaller
url = https://github.com/Vectorized/multicaller
1 change: 1 addition & 0 deletions examples/simple/remappings.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
multicaller/=../../tests/lib/multicaller/src/
216 changes: 216 additions & 0 deletions examples/simple/test/Multicaller.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
// SPDX-License-Identifier: AGPL-3.0
pragma solidity >=0.8.0 <0.9.0;

import {MulticallerWithSender} from "multicaller/MulticallerWithSender.sol";

import "forge-std/Test.sol";
import {SymTest} from "halmos-cheatcodes/SymTest.sol";

// An unoptimized reference implementation of MulticallerWithSender.
// This serves as a baseline for comparison against the optimized version, to verify the correctness of optimizations.
contract MulticallerWithSenderSpec {
error ArrayLengthsMismatch();

error Reentrancy();

address public sender;
bool public reentrancyUnlocked;

constructor() payable {
reentrancyUnlocked = true;
}

fallback(bytes calldata data) external payable returns (bytes memory) {
if (data.length > 0) revert();
return abi.encode(sender);
}

function aggregateWithSender(
address[] calldata targets,
bytes[] calldata data,
uint256[] calldata values
) external payable returns (bytes[] memory) {
if (targets.length != data.length || data.length != values.length) {
revert ArrayLengthsMismatch();
}

if (!reentrancyUnlocked) {
revert Reentrancy();
}

bytes[] memory results = new bytes[](data.length);

if (data.length == 0) {
return results;
}

// Lock
sender = msg.sender;
reentrancyUnlocked = false;

for (uint i = 0; i < data.length; i++) {
(bool success, bytes memory retdata) = targets[i].call{value: values[i]}(data[i]);
if (!success) {
_revertWithReturnData();
}
results[i] = retdata;
}

// Unlock
sender = address(0);
reentrancyUnlocked = true;

return results;
}

function _revertWithReturnData() internal pure {
assembly {
returndatacopy(0, 0, returndatasize())
revert(0, returndatasize())
}
}
}

contract MulticallerWithSenderMock is MulticallerWithSender {
// Provide public getters for the storage variables.
// Note: the variable order is set to align with the packing scheme used by the implementation.
address public sender;
bool public reentrancyUnlocked;
}

// A mock target contract that keeps track of external calls made via Multicaller
contract TargetMock is SymTest {
// Record of values received from each caller.
mapping (address => uint) private balanceOf;

fallback(bytes calldata data) external payable returns (bytes memory) {
balanceOf[msg.sender] += msg.value;

// Simulate deterministically random behaviors.
uint256 mode = msg.value & 255;
if (mode == 0) {
// Call multicaller fallback which should return the multicaller sender.
(bool success, bytes memory retdata) = msg.sender.call("");
return abi.encode(success, retdata);
} else if (mode == 1) {
// Reenter multicaller aggregateWithSender, which should revert.
(bool success, bytes memory retdata) = msg.sender.call(abi.encodeWithSelector(MulticallerWithSender.aggregateWithSender.selector, new address[](0), new bytes[](0), new uint256[](0)));
return abi.encode(success, retdata);
} else if (mode == 2) {
// Return the callvalue and calldata, which can then be retrieved later when checking the results of multicalls.
return abi.encode(msg.value, data);
} else {
revert();
}
}
}

// Check equivalence between the implementation and the reference spec.
// Establishing equivalence ensures that no mistakes are made in the optimizations made by the implementation.
contract MulticallerWithSenderSymTest is SymTest, Test {
MulticallerWithSenderMock impl; // implementation
MulticallerWithSenderSpec spec; // reference spec

// Slot number of the `balanceOf` mapping in TargetMock.
uint private constant _BALANCEOF_SLOT = 1;

address[] targetMocks;

function setUp() public {
impl = new MulticallerWithSenderMock();
spec = new MulticallerWithSenderSpec();

assert(impl.sender() == spec.sender());
assert(impl.reentrancyUnlocked() == spec.reentrancyUnlocked());

vm.deal(address(this), 100_000_000 ether);
vm.assume(address(impl).balance == address(spec).balance);
}

function _check_equivalence(bytes memory data) internal {
uint value = svm.createUint256("value");

(bool success_impl, bytes memory retdata_impl) = address(impl).call{value: value}(data);
(bool success_spec, bytes memory retdata_spec) = address(spec).call{value: value}(data);

// Check: `impl` succeeds if and only if `spec` succeeds.
assert(success_impl == success_spec);
// Check: the return data must be identical.
assert(keccak256(retdata_impl) == keccak256(retdata_spec));

// Check: the storage states must remain the same.
assert(impl.sender() == spec.sender());
assert(impl.reentrancyUnlocked() == spec.reentrancyUnlocked());

// Check: the remaining balances must be equal.
assert(address(impl).balance == address(spec).balance);
// Check: the total amounts sent to each target must be equal.
for (uint i = 0; i < targetMocks.length; i++) {
bytes32 target_balance_impl = vm.load(targetMocks[i], keccak256(abi.encode(impl, _BALANCEOF_SLOT)));
bytes32 target_balance_spec = vm.load(targetMocks[i], keccak256(abi.encode(spec, _BALANCEOF_SLOT)));
assert(target_balance_impl == target_balance_spec);
}
}

// Generate input arguments for `aggregateWithSender()`, given the specific sizes of dynamic arrays.
function _create_inputs(
uint targets_length,
uint data_length,
uint values_length,
uint data_size
) internal returns (bytes memory) {
// Construct `address[] targets` where `target[i]` may or may not be aliased with `target[i-1]`.
// This results in 2^(n-1) combinations of `targets` arrays, covering various alias scenarios.
address[] memory targets = new address[](targets_length);
for (uint i = 0; i < targets_length; i++) {
if (i == 0 || svm.createBool("unique_targets[i]")) {
address targetMock = address(new TargetMock());
targetMocks.push(targetMock);
targets[i] = targetMock;
} else {
targets[i] = targets[i-1]; // alias
}
}

// Construct `bytes[] data`, where `bytes data[i]` is created with the given `data_size`.
bytes[] memory data = new bytes[](data_length);
for (uint i = 0; i < data_length; i++) {
data[i] = svm.createBytes(data_size, "data[i]");
}

// Construct `uint256[] values`.
uint256[] memory values = new uint256[](values_length);
for (uint i = 0; i < values_length; i++) {
values[i] = svm.createUint256("values[i]");
}

return abi.encodeWithSelector(MulticallerWithSender.aggregateWithSender.selector, targets, data, values);
}

//
// Instantiations of the `_check_equivalence()` test for various combinations of dynamic array sizes.
//

function check_fallback_0() public { _check_equivalence(""); }
function check_fallback_1() public { _check_equivalence("1"); }

function check_1_0_0_1() public { _check_equivalence(_create_inputs(1, 0, 0, 1)); }
function check_0_0_0_1() public { _check_equivalence(_create_inputs(0, 0, 0, 1)); }
function check_1_1_1_1() public { _check_equivalence(_create_inputs(1, 1, 1, 1)); }
function check_2_2_2_1() public { _check_equivalence(_create_inputs(2, 2, 2, 1)); }

function check_1_0_0_32() public { _check_equivalence(_create_inputs(1, 0, 0, 32)); }
function check_0_0_0_32() public { _check_equivalence(_create_inputs(0, 0, 0, 32)); }
function check_1_1_1_32() public { _check_equivalence(_create_inputs(1, 1, 1, 32)); }
function check_2_2_2_32() public { _check_equivalence(_create_inputs(2, 2, 2, 32)); }

function check_1_0_0_31() public { _check_equivalence(_create_inputs(1, 0, 0, 31)); }
function check_0_0_0_31() public { _check_equivalence(_create_inputs(0, 0, 0, 31)); }
function check_1_1_1_31() public { _check_equivalence(_create_inputs(1, 1, 1, 31)); }
function check_2_2_2_31() public { _check_equivalence(_create_inputs(2, 2, 2, 31)); }

function check_1_0_0_65() public { _check_equivalence(_create_inputs(1, 0, 0, 65)); }
function check_0_0_0_65() public { _check_equivalence(_create_inputs(0, 0, 0, 65)); }
function check_1_1_1_65() public { _check_equivalence(_create_inputs(1, 1, 1, 65)); }
function check_2_2_2_65() public { _check_equivalence(_create_inputs(2, 2, 2, 65)); }
}
164 changes: 164 additions & 0 deletions tests/expected/simple.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,170 @@
"num_bounded_loops": null
}
],
"test/Multicaller.t.sol:MulticallerWithSenderSymTest": [
{
"name": "check_0_0_0_1()",
"exitcode": 0,
"num_models": 0,
"models": null,
"num_paths": null,
"time": null,
"num_bounded_loops": null
},
{
"name": "check_0_0_0_31()",
"exitcode": 0,
"num_models": 0,
"models": null,
"num_paths": null,
"time": null,
"num_bounded_loops": null
},
{
"name": "check_0_0_0_32()",
"exitcode": 0,
"num_models": 0,
"models": null,
"num_paths": null,
"time": null,
"num_bounded_loops": null
},
{
"name": "check_0_0_0_65()",
"exitcode": 0,
"num_models": 0,
"models": null,
"num_paths": null,
"time": null,
"num_bounded_loops": null
},
{
"name": "check_1_0_0_1()",
"exitcode": 0,
"num_models": 0,
"models": null,
"num_paths": null,
"time": null,
"num_bounded_loops": null
},
{
"name": "check_1_0_0_31()",
"exitcode": 0,
"num_models": 0,
"models": null,
"num_paths": null,
"time": null,
"num_bounded_loops": null
},
{
"name": "check_1_0_0_32()",
"exitcode": 0,
"num_models": 0,
"models": null,
"num_paths": null,
"time": null,
"num_bounded_loops": null
},
{
"name": "check_1_0_0_65()",
"exitcode": 0,
"num_models": 0,
"models": null,
"num_paths": null,
"time": null,
"num_bounded_loops": null
},
{
"name": "check_1_1_1_1()",
"exitcode": 0,
"num_models": 0,
"models": null,
"num_paths": null,
"time": null,
"num_bounded_loops": null
},
{
"name": "check_1_1_1_31()",
"exitcode": 0,
"num_models": 0,
"models": null,
"num_paths": null,
"time": null,
"num_bounded_loops": null
},
{
"name": "check_1_1_1_32()",
"exitcode": 0,
"num_models": 0,
"models": null,
"num_paths": null,
"time": null,
"num_bounded_loops": null
},
{
"name": "check_1_1_1_65()",
"exitcode": 0,
"num_models": 0,
"models": null,
"num_paths": null,
"time": null,
"num_bounded_loops": null
},
{
"name": "check_2_2_2_1()",
"exitcode": 0,
"num_models": 0,
"models": null,
"num_paths": null,
"time": null,
"num_bounded_loops": null
},
{
"name": "check_2_2_2_31()",
"exitcode": 0,
"num_models": 0,
"models": null,
"num_paths": null,
"time": null,
"num_bounded_loops": null
},
{
"name": "check_2_2_2_32()",
"exitcode": 0,
"num_models": 0,
"models": null,
"num_paths": null,
"time": null,
"num_bounded_loops": null
},
{
"name": "check_2_2_2_65()",
"exitcode": 0,
"num_models": 0,
"models": null,
"num_paths": null,
"time": null,
"num_bounded_loops": null
},
{
"name": "check_fallback_0()",
"exitcode": 0,
"num_models": 0,
"models": null,
"num_paths": null,
"time": null,
"num_bounded_loops": null
},
{
"name": "check_fallback_1()",
"exitcode": 0,
"num_models": 0,
"models": null,
"num_paths": null,
"time": null,
"num_bounded_loops": null
}
],
"test/TotalPrice.t.sol:TotalPriceTest": [
{
"name": "check_eq_totalPriceFixed_totalPriceConservative(uint96,uint32)",
Expand Down
1 change: 1 addition & 0 deletions tests/lib/multicaller
Submodule multicaller added at b4a0dd

0 comments on commit 70ddda2

Please sign in to comment.