Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 101 additions & 0 deletions src/accounts/ERC7821.sol
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ contract ERC7821 is Receiver {
bytes data; // Calldata to send with the call.
}

struct CallSansTo {
uint256 value; // Amount of native currency (i.e. Ether) to send.
bytes data; // Calldata to send with the call.
}

/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* CUSTOM ERRORS */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
Expand Down Expand Up @@ -55,6 +60,7 @@ contract ERC7821 is Receiver {
/// - `0x01000000000000000000...`: Single batch. Does not support optional `opData`.
/// - `0x01000000000078210001...`: Single batch. Supports optional `opData`.
/// - `0x01000000000078210002...`: Batch of batches.
/// - `0x01000000000078210003...`: Single batch with common `to` address and optional `opData`.
///
/// For the "batch of batches" mode, each batch will be recursively passed into
/// `execute` internally with mode `0x01000000000078210001...`.
Expand All @@ -73,8 +79,10 @@ contract ERC7821 is Receiver {
function execute(bytes32 mode, bytes calldata executionData) public payable virtual {
uint256 id = _executionModeId(mode);
if (id == 3) return _executeBatchOfBatches(mode, executionData);
if (id == 4) return _executeBatchCommonTo(mode, executionData);
Call[] calldata calls;
bytes calldata opData;

/// @solidity memory-safe-assembly
assembly {
if iszero(id) {
Expand Down Expand Up @@ -126,7 +134,40 @@ contract ERC7821 is Receiver {
id := eq(m, 0x01000000000000000000) // 1.
id := or(shl(1, eq(m, 0x01000000000078210001)), id) // 2.
id := or(mul(3, eq(m, 0x01000000000078210002)), id) // 3.
id := or(mul(4, eq(m, 0x01000000000078210003)), id) // 4.
}
}

/// @dev For execution of a batch of batches with a common `to` address.
/// @dev if to == address(0), it will be replaced with address(this)
/// Execution Data: abi.encode(address to, CallSansTo[] calls, bytes opData)
function _executeBatchCommonTo(bytes32 mode, bytes calldata executionData) internal virtual {
address to;
CallSansTo[] calldata calls;
bytes calldata opData;

/// @solidity memory-safe-assembly
assembly {
to := calldataload(executionData.offset)

let callOffset :=
add(executionData.offset, calldataload(add(0x20, executionData.offset)))
calls.offset := add(callOffset, 0x20)
calls.length := calldataload(callOffset)

// This line is needed to ensure that opdata is valid in all code paths.
// Otherwise the compiler complains.
opData.length := 0
// If the offset of `executionData` allows for `opData`, and the mode supports it.
if gt(calldataload(add(0x20, executionData.offset)), 0x40) {
let opDataOffset :=
add(executionData.offset, calldataload(add(0x40, executionData.offset)))
opData.offset := add(opDataOffset, 0x20)
opData.length := calldataload(opDataOffset)
}
}

_execute(mode, executionData, to, calls, opData);
}

/// @dev For execution of a batch of batches.
Expand Down Expand Up @@ -190,6 +231,29 @@ contract ERC7821 is Receiver {
revert(); // In your override, replace this with logic to operate on `opData`.
}

/// @dev Executes the calls.
/// Reverts and bubbles up error if any call fails.
/// The `mode` and `executionData` are passed along in case there's a need to use them.
function _execute(
bytes32 mode,
bytes calldata executionData,
address to,
CallSansTo[] calldata calls,
bytes calldata opData
) internal virtual {
// Silence compiler warning on unused variables.
mode = mode;
executionData = executionData;
// Very basic auth to only allow this contract to be called by itself.
// Override this function to perform more complex auth with `opData`.
if (opData.length == uint256(0)) {
require(msg.sender == address(this));
// Remember to return `_execute(calls, extraData)` when you override this function.
return _execute(calls, to, bytes32(0));
}
revert(); // In your override, replace this with logic to operate on `opData`.
}

/// @dev Executes the calls.
/// Reverts and bubbles up error if any call fails.
/// `extraData` can be any supplementary data (e.g. a memory pointer, some hash).
Expand All @@ -204,6 +268,26 @@ contract ERC7821 is Receiver {
}
}

/// @dev Executes the calls.
/// Reverts and bubbles up error if any call fails.
/// `extraData` can be any supplementary data (e.g. a memory pointer, some hash).
function _execute(CallSansTo[] calldata calls, address to, bytes32 keyHash) internal virtual {
unchecked {
uint256 i;
// If `to` is address(0), it will be replaced with address(this)
/// @solidity memory-safe-assembly
assembly {
let t := shr(96, shl(96, to))
to := or(mul(address(), iszero(t)), t)
}
if (calls.length == uint256(0)) return;
do {
(uint256 value, bytes calldata data) = _get(calls, i);
_execute(to, value, data, keyHash);
} while (++i != calls.length);
}
}

/// @dev Executes the call.
/// Reverts and bubbles up error if any call fails.
/// `extraData` can be any supplementary data (e.g. a memory pointer, some hash).
Expand All @@ -224,6 +308,23 @@ contract ERC7821 is Receiver {
}
}

/// @dev Convenience function for getting `calls[i]`, without bounds checks.
function _get(CallSansTo[] calldata calls, uint256 i)
internal
view
virtual
returns (uint256 value, bytes calldata data)
{
/// @solidity memory-safe-assembly
assembly {
let c := add(calls.offset, calldataload(add(calls.offset, shl(5, i))))
value := calldataload(c)
let o := add(c, calldataload(add(c, 0x20)))
data.offset := add(o, 0x20)
data.length := calldataload(o)
}
}

/// @dev Convenience function for getting `calls[i]`, without bounds checks.
function _get(Call[] calldata calls, uint256 i)
internal
Expand Down
109 changes: 108 additions & 1 deletion test/ERC7821.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ contract ERC7821Test is SoladyTest {
address target;

bytes32 internal constant _SUPPORTED_MODE = bytes10(0x01000000000078210001);

bytes32 internal constant _COMMON_TO_MODE = bytes10(0x01000000000078210003);
bytes[] internal _bytes;

function setUp() public {
Expand Down Expand Up @@ -54,6 +54,24 @@ contract ERC7821Test is SoladyTest {
mbe.execute{value: _totalValue(calls)}(_SUPPORTED_MODE, data);
}

function testERC7821CommonToGas() public {
vm.pauseGasMetering();
vm.deal(address(this), 1 ether);

ERC7821.CallSansTo[] memory calls = new ERC7821.CallSansTo[](2);

calls[0].value = 123;
calls[0].data = abi.encodeWithSignature("returnsBytes(bytes)", "hehe");

calls[1].value = 789;
calls[1].data = abi.encodeWithSignature("returnsHash(bytes)", "lol");

bytes memory data = abi.encode(target, calls);
vm.resumeGasMetering();

mbe.execute{value: _totalValue(calls)}(_COMMON_TO_MODE, data);
}

function testERC7821(bytes memory opData) public {
vm.deal(address(this), 1 ether);

Expand All @@ -72,6 +90,23 @@ contract ERC7821Test is SoladyTest {
assertEq(mbe.lastOpData(), opData);
}

function testERC7821CommonTo(bytes memory opData) public {
vm.deal(address(this), 1 ether);

ERC7821.CallSansTo[] memory calls = new ERC7821.CallSansTo[](2);
calls[0].value = 123;
calls[0].data = abi.encodeWithSignature("returnsBytes(bytes)", "hehe");

calls[1].value = 789;
calls[1].data = abi.encodeWithSignature("returnsHash(bytes)", "lol");

mbe.execute{value: _totalValue(calls)}(
_COMMON_TO_MODE, _encodeCommonTo(target, calls, opData)
);

assertEq(mbe.lastOpData(), opData);
}

function testERC7821ForRevert() public {
ERC7821.Call[] memory calls = new ERC7821.Call[](1);
calls[0].to = target;
Expand All @@ -82,6 +117,15 @@ contract ERC7821Test is SoladyTest {
mbe.execute{value: _totalValue(calls)}(_SUPPORTED_MODE, _encode(calls, ""));
}

function testERC7821CommonToForRevert() public {
ERC7821.CallSansTo[] memory calls = new ERC7821.CallSansTo[](1);
calls[0].value = 0;
calls[0].data = abi.encodeWithSignature("revertsWithCustomError()");

vm.expectRevert(CustomError.selector);
mbe.execute{value: _totalValue(calls)}(_COMMON_TO_MODE, _encodeCommonTo(target, calls, ""));
}

function _encode(ERC7821.Call[] memory calls, bytes memory opData)
internal
returns (bytes memory)
Expand All @@ -90,6 +134,14 @@ contract ERC7821Test is SoladyTest {
return abi.encode(calls, opData);
}

function _encodeCommonTo(address to, ERC7821.CallSansTo[] memory calls, bytes memory opData)
internal
returns (bytes memory)
{
if (_randomChance(2) && opData.length == 0) return abi.encode(to, calls);
return abi.encode(to, calls, opData);
}

struct Payload {
bytes data;
uint256 mode;
Expand Down Expand Up @@ -125,6 +177,35 @@ contract ERC7821Test is SoladyTest {
}
}

function testERC7821CommonTo(bytes32) public {
vm.deal(address(this), 1 ether);

ERC7821.CallSansTo[] memory calls = new ERC7821.CallSansTo[](_randomUniform() & 3);
Payload[] memory payloads = new Payload[](calls.length);

for (uint256 i; i < calls.length; ++i) {
calls[i].value = _randomUniform() & 0xff;
bytes memory data = _truncateBytes(_randomBytes(), 0x1ff);
payloads[i].data = data;
if (_randomChance(2)) {
payloads[i].mode = 0;
calls[i].data = abi.encodeWithSignature("returnsBytes(bytes)", data);
} else {
payloads[i].mode = 1;
calls[i].data = abi.encodeWithSignature("returnsHash(bytes)", data);
}
}

mbe.executeDirect{value: _totalValue(calls)}(calls, target);

if (calls.length != 0 && _randomChance(32)) {
calls[_randomUniform() % calls.length].data =
abi.encodeWithSignature("revertsWithCustomError()");
vm.expectRevert(CustomError.selector);
mbe.executeDirect{value: _totalValue(calls)}(calls, target);
}
}

function _totalValue(ERC7821.Call[] memory calls) internal pure returns (uint256 result) {
unchecked {
for (uint256 i; i < calls.length; ++i) {
Expand All @@ -133,6 +214,18 @@ contract ERC7821Test is SoladyTest {
}
}

function _totalValue(ERC7821.CallSansTo[] memory calls)
internal
pure
returns (uint256 result)
{
unchecked {
for (uint256 i; i < calls.length; ++i) {
result += calls[i].value;
}
}
}

function testERC7821ExecuteBatchOfBatches() public {
bytes32 mode = bytes32(0x0100000000007821000200000000000000000000000000000000000000000000);
bytes[] memory batchBytes = new bytes[](3);
Expand Down Expand Up @@ -182,4 +275,18 @@ contract ERC7821Test is SoladyTest {
function pushBytes(bytes memory x) public {
_bytes.push(x);
}

function testERC7821CommonToWithZeroAddress() public {
// Test that when to=address(0), it gets replaced with address(this) (the MockERC7821 contract)
// We'll call executeDirect which directly calls the internal _execute function
ERC7821.CallSansTo[] memory calls = new ERC7821.CallSansTo[](1);
calls[0].value = 0;
calls[0].data = abi.encodeWithSignature("setAuthorizedCaller(address,bool)", address(0x123), true);

// This should replace address(0) with address(mbe) and call setAuthorizedCaller on itself
mbe.executeDirect(calls, address(0));

// Verify the call succeeded by checking that address(0x123) is now authorized
assertTrue(mbe.isAuthorizedCaller(address(0x123)));
}
}
18 changes: 18 additions & 0 deletions test/utils/mocks/MockERC7821.sol
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,17 @@ contract MockERC7821 is ERC7821, Brutalizer {
_execute(calls, bytes32(0));
}

function _execute(
bytes32,
bytes calldata,
address to,
CallSansTo[] calldata calls,
bytes calldata opData
) internal virtual override {
lastOpData = opData;
_execute(calls, to, bytes32(0));
}

function execute(bytes32 mode, bytes calldata executionData) public payable virtual override {
if (!isAuthorizedCaller[msg.sender]) revert Unauthorized();
super.execute(mode, executionData);
Expand All @@ -34,6 +45,13 @@ contract MockERC7821 is ERC7821, Brutalizer {
_checkMemory();
}

function executeDirect(CallSansTo[] calldata calls, address to) public payable virtual {
_misalignFreeMemoryPointer();
_brutalizeMemory();
_execute(calls, to, bytes32(0));
_checkMemory();
}

function setAuthorizedCaller(address target, bool status) public {
isAuthorizedCaller[target] = status;
}
Expand Down
Loading