Skip to content

Commit

Permalink
add: first pass at implementing OSXAdapter
Browse files Browse the repository at this point in the history
* Almost certainly broken.
* no tests implemented.
* Pulled code from Roles mod to unwrap multisend transactions, need to revisit and write tests to ensure payloads are converted correctly.
  • Loading branch information
auryn-macmillan committed Jul 18, 2024
1 parent 9c5f6d3 commit a04bcc6
Show file tree
Hide file tree
Showing 9 changed files with 344 additions and 49 deletions.
18 changes: 18 additions & 0 deletions contracts/IOSx.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity >=0.8.26 <0.9.0;

import "./Types.sol";

interface IOSx {
/// @notice Executes a list of actions. If a zero allow-failure map is provided, a failing action reverts the entire execution. If a non-zero allow-failure map is provided, allowed actions can fail without the entire call being reverted.
/// @param _callId The ID of the call. The definition of the value of `callId` is up to the calling contract and can be used, e.g., as a nonce.
/// @param _actions The array of actions.
/// @param _allowFailureMap A bitmap allowing execution to succeed, even if individual actions might revert. If the bit at index `i` is 1, the execution succeeds even if the `i`th action reverts. A failure map value of 0 requires every action to not revert.
/// @return The array of results obtained from the executed actions in `bytes`.
/// @return The resulting failure map containing the actions have actually failed.
function execute(
bytes32 _callId,
Action[] memory _actions,
uint256 _allowFailureMap
) external returns (bytes[] memory, uint256);
}
117 changes: 117 additions & 0 deletions contracts/MultiSendUnwrapper.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity >=0.8.26 <0.9.0;

import "./Types.sol";

contract MultiSendUnwrapper is ITransactionUnwrapper {
uint256 private constant OFFSET_START = 68;

error UnsupportedMode();
error MalformedHeader();
error MalformedBody();

function unwrap(
address,
uint256 value,
bytes calldata data,
Enum.Operation operation
) external pure returns (UnwrappedTransaction[] memory) {
if (value != 0) {
revert UnsupportedMode();
}
if (operation != Enum.Operation.DelegateCall) {
revert UnsupportedMode();
}
_validateHeader(data);
uint256 count = _validateEntries(data);
return _unwrapEntries(data, count);
}

function _validateHeader(bytes calldata data) private pure {
// first 4 bytes are the selector for multiSend(bytes)
if (bytes4(data) != IMultiSend.multiSend.selector) {
revert MalformedHeader();
}

// the following 32 bytes are the offset to the bytes param
// (always 0x20)
if (bytes32(data[4:]) != bytes32(uint256(0x20))) {
revert MalformedHeader();
}

// the following 32 bytes are the length of the bytes param
uint256 length = uint256(bytes32(data[36:]));

// validate that the total calldata length matches
// it's the 4 + 32 + 32 bytes checked above + the <length> bytes
// padded to a multiple of 32
if (4 + _ceil32(32 + 32 + length) != data.length) {
revert MalformedHeader();
}
}

function _validateEntries(bytes calldata data) private pure returns (uint256 count) {
uint256 offset = OFFSET_START;

// data is padded to 32 bytes we can't simply do offset < data.length
for (; offset + 32 < data.length; ) {
// Per transaction:
// Operation 1 bytes
// To 20 bytes
// Value 32 bytes
// Length 32 bytes
// Data Length bytes
uint8 operation = uint8(bytes1(data[offset:]));
if (operation > 1) {
revert MalformedBody();
}

uint256 length = uint256(bytes32(data[offset + 53:]));
if (offset + 85 + length > data.length) {
revert MalformedBody();
}

offset += 85 + length;
count++;
}

if (count == 0) {
revert MalformedBody();
}
}

function _unwrapEntries(
bytes calldata data,
uint256 count
) private pure returns (UnwrappedTransaction[] memory result) {
result = new UnwrappedTransaction[](count);

uint256 offset = OFFSET_START;
for (uint256 i; i < count; ) {
result[i].operation = Enum.Operation(uint8(bytes1(data[offset:])));
offset += 1;

result[i].to = address(bytes20(data[offset:]));
offset += 20;

result[i].value = uint256(bytes32(data[offset:]));
offset += 32;

uint256 size = uint256(bytes32(data[offset:]));
offset += 32;

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

offset += size;

unchecked {
++i;
}
}
}

function _ceil32(uint256 length) private pure returns (uint256) {
// pad size. Source: http://www.cs.nott.ac.uk/~psarb2/G51MPC/slides/NumberLogic.pdf
return ((length + 32 - 1) / 32) * 32;
}
}
29 changes: 0 additions & 29 deletions contracts/MyModule.sol

This file was deleted.

141 changes: 141 additions & 0 deletions contracts/OSXAdapter.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity >=0.8.26 <0.9.0;

import {Modifier} from "@gnosis.pm/zodiac/contracts/core/Modifier.sol";
import {Enum} from "@gnosis.pm/zodiac/contracts/core/Module.sol";
import {IOSx} from "./IOSx.sol";
import "./Types.sol";

contract OSXAdapter is Modifier {
/// @notice The ID of the permission required to call the OSx `execute` function.
bytes32 public constant EXECUTE_PERMISSION_ID = keccak256("EXECUTE_PERMISSION");

/// @notice Maps allowed multisend addresses to their corresponding transaction unwrappers.
/// @dev Delegate calls to mapped addresses will be unwrapped into an array of calls.
mapping(address multisend => ITransactionUnwrapper transactionUnwrapper) public transactionUnwrappers;

event TransactionUnwrapperSet(address multisendAddress, ITransactionUnwrapper transactionUnwrapper);

error DelegateCallNotAllowed();
error MultisendAddressNotAllowed();
error TransactionUnwrapperAlreadySet();

constructor(address _owner, address _avatar, address _target) {
bytes memory initializeParams = abi.encode(_owner, _avatar, _target);
setUp(initializeParams);
}

/// @dev Initialize function, will be triggered when a new proxy is deployed
/// @param initializeParams Parameters of initialization encoded
function setUp(bytes memory initializeParams) public override initializer {
__Ownable_init(msg.sender);
(address _owner, address _avatar, address _target) = abi.decode(initializeParams, (address, address, address));

setAvatar(_avatar);
setTarget(_target);
transferOwnership(_owner);
}

function execTransactionFromModule(
address to,
uint256 value,
bytes calldata data,
Enum.Operation operation
) public override moduleOnly returns (bool success) {
success = exec(to, value, data, operation);
}

function execTransactionFromModuleReturnData(
address to,
uint256 value,
bytes calldata data,
Enum.Operation operation
) public override moduleOnly returns (bool success, bytes memory returnData) {
(success, returnData) = execAndReturnData(to, value, data, operation);
}

function setTransactionUnwrapper(
address multisendAddress,
ITransactionUnwrapper transactionUnwrapper
) public onlyOwner {
require(transactionUnwrappers[multisendAddress] != transactionUnwrapper, TransactionUnwrapperAlreadySet());
transactionUnwrappers[multisendAddress] = transactionUnwrapper;
emit TransactionUnwrapperSet(multisendAddress, transactionUnwrapper);
}

/// @dev Passes a transaction to be executed by the avatar.
/// @notice Can only be called by this contract.
/// @param to Destination address of module transaction.
/// @param value Ether value of module transaction.
/// @param data Data payload of module transaction.
/// @param operation Operation type of module transaction: 0 == call, 1 == delegate call.
function exec(
address to,
uint256 value,
bytes memory data,
Enum.Operation operation
) internal override returns (bool success) {
Action[] memory actions = convertTransaction(to, value, data, operation);
IOSx(target).execute(bytes32(0), actions, 0);
success = true;
}

/// @dev Passes a transaction to be executed by the target and returns data.
/// @notice Can only be called by this contract.
/// @param to Destination address of module transaction.
/// @param value Ether value of module transaction.
/// @param data Data payload of module transaction.
/// @param operation Operation type of module transaction: 0 == call, 1 == delegate call.
function execAndReturnData(
address to,
uint256 value,
bytes memory data,
Enum.Operation operation
) internal override returns (bool, bytes memory) {
Action[] memory actions = convertTransaction(to, value, data, operation);
(bytes[] memory returnData, ) = IOSx(target).execute(bytes32(0), actions, 0);
return (true, abi.encode(returnData));
}

function convertTransaction(
address to,
uint256 value,
bytes memory data,
Enum.Operation operation
) private view returns (Action[] memory actions) {
if (operation == Enum.Operation.DelegateCall) {
ITransactionUnwrapper transactionUnwrapper = transactionUnwrappers[to];
require(transactionUnwrapper != ITransactionUnwrapper(address(0)), MultisendAddressNotAllowed());

UnwrappedTransaction[] memory unwrappedTransactions = transactionUnwrapper.unwrap(
to,
value,
data,
operation
);

for (uint i = 0; i < unwrappedTransactions.length; i++) {
actions[i] = convert(
unwrappedTransactions[i].to,
unwrappedTransactions[i].value,
unwrappedTransactions[i].data,
unwrappedTransactions[i].operation
);
}
} else {
actions[0] = convert(to, value, data, operation);
}
}

function convert(
address to,
uint256 value,
bytes memory data,
Enum.Operation operation
) private pure returns (Action memory action) {
require(operation == Enum.Operation.Call, DelegateCallNotAllowed());
action.to = to;
action.value = value;
action.data = data;
}
}
37 changes: 37 additions & 0 deletions contracts/Types.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity >=0.8.26 <0.9.0;

import "@gnosis.pm/safe-contracts/contracts/common/Enum.sol";

interface IMultiSend {
function multiSend(bytes memory transactions) external payable;
}

struct UnwrappedTransaction {
Enum.Operation operation;
address to;
uint256 value;
bytes data;
// We wanna deal in calldata slices. We return location, let invoker slice
// uint256 dataLocation;
// uint256 dataSize;
}

/// @notice The action struct to be consumed by the DAO's `execute` function resulting in an external call.
/// @param to The address to call.
/// @param value The native token value to be sent with the call.
/// @param data The bytes-encoded function selector and calldata for the call.
struct Action {
address to;
uint256 value;
bytes data;
}

interface ITransactionUnwrapper {
function unwrap(
address to,
uint256 value,
bytes calldata data,
Enum.Operation operation
) external view returns (UnwrappedTransaction[] memory result);
}
6 changes: 1 addition & 5 deletions contracts/test/TestAvatar.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,7 @@ contract TestAvatar {
module = _module;
}

function exec(
address payable to,
uint256 value,
bytes calldata data
) external {
function exec(address payable to, uint256 value, bytes calldata data) external {
bool success;
bytes memory response;
(success, response) = to.call{value: value}(data);
Expand Down
8 changes: 4 additions & 4 deletions deploy/01_mastercopy_module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { DeployFunction } from "hardhat-deploy/types"
import { HardhatRuntimeEnvironment } from "hardhat/types"
import { createFactory, deployViaFactory } from "../factories/eip2470"

import MODULE_CONTRACT_ARTIFACT from "../artifacts/contracts/MyModule.sol/MyModule.json"
import MODULE_CONTRACT_ARTIFACT from "../artifacts/contracts/OSXAdapter.sol/OSXAdapter.json"

const FirstAddress = "0x0000000000000000000000000000000000000001"

Expand All @@ -14,12 +14,12 @@ const deploy: DeployFunction = async function (hre: HardhatRuntimeEnvironment) {

await createFactory(deployer)

const MyModule = await ethers.getContractFactory("MyModule")
const tx = await MyModule.getDeployTransaction(FirstAddress, FirstAddress)
const OSXAdapter = await ethers.getContractFactory("OSXAdapter")
const tx = await OSXAdapter.getDeployTransaction(FirstAddress, FirstAddress, FirstAddress)

const mastercopy = await deployViaFactory({ bytecode: tx.data, salt: ZeroHash }, deployer)

hre.deployments.save("MyModuleMastercopy", {
hre.deployments.save("OSXAdapterMastercopy", {
abi: MODULE_CONTRACT_ARTIFACT.abi,
address: mastercopy,
})
Expand Down
Loading

0 comments on commit a04bcc6

Please sign in to comment.