Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Delegate call support #23

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
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
4 changes: 4 additions & 0 deletions contracts/Libraries/Events.sol
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ contract Events {
emit LogUint(message);
}

function logUintPayable(uint256 message) external payable {
emit LogUint(message);
}

fallback() external payable virtual {
if (msg.value > 0) emit LogUint(msg.value);
if (msg.data.length > 0) emit LogBytes(msg.data);
Expand Down
201 changes: 201 additions & 0 deletions contracts/StatelessVM.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
// SPDX-License-Identifier: GPL-3.0-only

/**********************************************************************************************
* WARNING! *
* This contract should only be imported by contracts that do not depend on state storage, *
* as it supports delegate calls which can alter state and potentially break your contract! *
* *
**********************************************************************************************/


pragma solidity ^0.8.16;

import "./CommandBuilder.sol";

abstract contract StatelessVM {
using CommandBuilder for bytes[];

uint256 constant FLAG_CT_DELEGATECALL = 0x00;
uint256 constant FLAG_CT_CALL = 0x01;
uint256 constant FLAG_CT_STATICCALL = 0x02;
uint256 constant FLAG_CT_VALUECALL = 0x03;
uint256 constant FLAG_CT_MASK = 0x03;
uint256 constant FLAG_DATA = 0x20;
uint256 constant FLAG_EXTENDED_COMMAND = 0x40;
uint256 constant FLAG_TUPLE_RETURN = 0x80;

uint256 constant SHORT_COMMAND_FILL =
0x000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF;

error ExecutionFailed(
uint256 command_index,
address target,
string message
);

function _execute(bytes32[] calldata commands, bytes[] memory state)
internal
returns (bytes[] memory)
{
bytes32 command;
uint256 flags;
bytes32 indices;

bool success;
bytes memory outData;

uint256 commandsLength = commands.length;
uint256 indicesLength;
for (uint256 i; i < commandsLength; i = _uncheckedIncrement(i)) {
command = commands[i];
flags = uint256(uint8(bytes1(command << 32)));

if (flags & FLAG_EXTENDED_COMMAND != 0) {
i = _uncheckedIncrement(i);
indices = commands[i];
indicesLength = 32;
} else {
indices = bytes32(uint256(command << 40) | SHORT_COMMAND_FILL);
indicesLength = 6;
}

if (flags & FLAG_CT_MASK == FLAG_CT_DELEGATECALL) {
(success, outData) = address(uint160(uint256(command))) // target
.delegatecall(
// inputs
flags & FLAG_DATA == 0
? state.buildInputs(
bytes4(command), // selector
indices,
indicesLength
)
: state[
uint8(bytes1(indices)) &
CommandBuilder.IDX_VALUE_MASK
]
);
} else if (flags & FLAG_CT_MASK == FLAG_CT_CALL) {
(success, outData) = address(uint160(uint256(command))).call( // target
// inputs
flags & FLAG_DATA == 0
? state.buildInputs(
bytes4(command), // selector
indices,
indicesLength
)
: state[
uint8(bytes1(indices)) &
CommandBuilder.IDX_VALUE_MASK
]
);
} else if (flags & FLAG_CT_MASK == FLAG_CT_STATICCALL) {
(success, outData) = address(uint160(uint256(command))) // target
.staticcall(
// inputs
flags & FLAG_DATA == 0
? state.buildInputs(
bytes4(command), // selector
indices,
indicesLength
)
: state[
uint8(bytes1(indices)) &
CommandBuilder.IDX_VALUE_MASK
]
);
} else if (flags & FLAG_CT_MASK == FLAG_CT_VALUECALL) {
bytes memory v = state[
uint8(bytes1(indices)) &
CommandBuilder.IDX_VALUE_MASK
];
require(v.length == 32, "Value must be 32 bytes");
uint256 callEth = uint256(bytes32(v));
(success, outData) = address(uint160(uint256(command))).call{ // target
value: callEth
}(
// inputs
flags & FLAG_DATA == 0
? state.buildInputs(
bytes4(command), // selector
indices << 8, // skip value input
indicesLength - 1 // max indices length reduced by value input
)
: state[
uint8(bytes1(indices << 8)) & // first byte after value input
CommandBuilder.IDX_VALUE_MASK
]
);
} else {
revert("Invalid calltype");
}

if (!success) {
string memory message = "Unknown";
if (outData.length > 68) {
// This might be an error message, parse the outData
// Estimate the bytes length of the possible error message
uint256 estimatedLength = _estimateBytesLength(outData, 68);
// Remove selector. First 32 bytes should be a pointer that indicates the start of data in memory
assembly {
outData := add(outData, 4)
}
uint256 pointer = uint256(bytes32(outData));
if (pointer == 32) {
// Remove pointer. If it is a string, the next 32 bytes will hold the size
assembly {
outData := add(outData, 32)
}
uint256 size = uint256(bytes32(outData));
// If the size variable is the same as the estimated bytes length, we can be fairly certain
// this is a dynamic string, so convert the bytes to a string and emit the message. While an
// error function with 3 static parameters is capable of producing a similar output, there is
// low risk of a contract unintentionally emitting a message.
if (size == estimatedLength) {
// Remove size. The remaining data should be the string content
assembly {
outData := add(outData, 32)
}
message = string(outData);
}
}
}
revert ExecutionFailed({
command_index: flags & FLAG_EXTENDED_COMMAND == 0
? i
: i - 1,
target: address(uint160(uint256(command))),
message: message
});
}

if (flags & FLAG_TUPLE_RETURN != 0) {
state.writeTuple(bytes1(command << 88), outData);
} else {
state = state.writeOutputs(bytes1(command << 88), outData);
}
}
return state;
}

function _estimateBytesLength(bytes memory data, uint256 pos) internal pure returns (uint256 estimate) {
uint256 length = data.length;
estimate = length - pos; // Assume length equals alloted space
for (uint256 i = pos; i < length; ) {
if (data[i] == 0) {
// Zero bytes found, adjust estimated length
estimate = i - pos;
break;
}
unchecked {
++i;
}
}
}

function _uncheckedIncrement(uint256 i) private pure returns (uint256) {
unchecked {
++i;
}
return i;
}
}
6 changes: 3 additions & 3 deletions contracts/test/Struct.sol
Original file line number Diff line number Diff line change
Expand Up @@ -140,9 +140,9 @@ contract Struct is Events, Math {

function returnComplexStruct(
DataStruct memory dataStruct,
UserStruct memory userStruct,
uint256 amount,
uint256 timestamp
UserStruct memory,
uint256,
uint256
)
external
returns (bytes32)
Expand Down
16 changes: 16 additions & 0 deletions contracts/test/TestableStatelessVM.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity ^0.8.16;

import "../StatelessVM.sol";

contract TestableStatelessVM is StatelessVM {
function execute(bytes32[] calldata commands, bytes[] memory state)
public
payable
returns (bytes[] memory)
{
return _execute(commands, state);
}

receive() external payable {}
}
Loading
Loading