Skip to content

Commit

Permalink
Merge pull request #12 from 5afe/feature-4-module-mediator
Browse files Browse the repository at this point in the history
Module mediator
  • Loading branch information
akshay-ap authored Jul 4, 2023
2 parents f85de6f + 6a0c4ba commit 63686c8
Show file tree
Hide file tree
Showing 19 changed files with 1,478 additions and 42 deletions.
16 changes: 16 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
module.exports = {
env: {
browser: true,
es2021: true,
node: true,
},
extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended", "plugin:prettier/recommended"],
overrides: [],
parser: "@typescript-eslint/parser",
parserOptions: {
ecmaVersion: "latest",
sourceType: "module",
},
plugins: ["@typescript-eslint", "no-only-tests"],
rules: {},
};
3 changes: 3 additions & 0 deletions .solcover.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports = {
skipFiles: ['test/TestExecutor.sol', 'test/TestModule.sol', 'test/TestFallbackReceiver.sol']
};
2 changes: 1 addition & 1 deletion contracts/DataTypes.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
pragma solidity ^0.8.18;

struct SafeProtocolAction {
address to;
address payable to;
uint256 value;
bytes data;
}
Expand Down
248 changes: 248 additions & 0 deletions contracts/SafeProtocolMediator.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity ^0.8.18;
import {ISafeProtocolMediator} from "./interfaces/Mediator.sol";
import {ISafeProtocolModule} from "./interfaces/Components.sol";

import {ISafe} from "./interfaces/Accounts.sol";
import {SafeProtocolAction, SafeTransaction, SafeRootAccess} from "./DataTypes.sol";

/**
* @title SafeProtocolMediator contract allows Safe users to set module through a Mediator rather than directly enabling a module on Safe.
* Users have to first enable SafeProtocolMediator as a module on a Safe and then enable other modules through the mediator.
*/
contract SafeProtocolMediator is ISafeProtocolMediator {
address internal constant SENTINEL_MODULES = address(0x1);

/**
* @notice Mapping of a mapping what stores information about modules that are enabled per Safe.
* address (Safe address) => address (component address) => EnabledModuleInfo
*/
mapping(address => mapping(address => ModuleAccessInfo)) public enabledModules;
struct ModuleAccessInfo {
bool rootAddressGranted;
address nextModulePointer;
}

// Events
event ActionsExecuted(address indexed safe, bytes32 metaHash, uint256 nonce);
event RootAccessActionExecuted(address indexed safe, bytes32 metaHash);
event ModuleEnabled(address indexed safe, address indexed module, bool allowRootAccess);
event ModuleDisabled(address indexed safe, address indexed module);

// Errors
error ModuleRequiresRootAccess(address sender);
error MoudleNotEnabled(address module);
error ModuleEnabledOnlyForRootAccess(address module);
error ModuleAccessMismatch(address module, bool requiresRootAccess, bool providedValue);
error ActionExecutionFailed(address safe, bytes32 metaHash, uint256 index);
error RootAccessActionExecutionFailed(address safe, bytes32 metaHash);
error ModuleAlreadyEnabled(address safe, address module);
error InvalidModuleAddress(address module);
error InvalidPrevModuleAddress(address module);
error ZeroPageSizeNotAllowed();

modifier onlyEnabledModule(address safe) {
if (enabledModules[safe][msg.sender].nextModulePointer == address(0)) {
revert MoudleNotEnabled(msg.sender);
}
_;
}

modifier noZeroOrSentinelModule(address module) {
if (module == address(0) || module == SENTINEL_MODULES) {
revert InvalidModuleAddress(module);
}
_;
}

/**
* @notice This function executes non-delegate call(s) on a safe if the module is enabled on the Safe.
* If any one of the actions fail, the transaction reverts.
* @param safe A Safe instance
* @param transaction A struct of type SafeTransaction containing information of about the action(s) to be executed.
* Users can add logic to validate metahash through a transaction guard.
* @return data bytes types containing the result of the executed action.
*/
function executeTransaction(
ISafe safe,
SafeTransaction calldata transaction
) external override onlyEnabledModule(address(safe)) returns (bytes[] memory data) {
address safeAddress = address(safe);
data = new bytes[](transaction.actions.length);
uint256 length = transaction.actions.length;
for (uint256 i = 0; i < length; ++i) {
SafeProtocolAction calldata safeProtocolAction = transaction.actions[i];
(bool isActionSuccessful, bytes memory resultData) = safe.execTransactionFromModuleReturnData(
safeProtocolAction.to,
safeProtocolAction.value,
safeProtocolAction.data,
0
);

// Even if one action fails, revert the transaction.
if (!isActionSuccessful) {
revert ActionExecutionFailed(safeAddress, transaction.metaHash, i);
} else {
data[i] = resultData;
}
}

emit ActionsExecuted(safeAddress, transaction.metaHash, transaction.nonce);
}

/**
* @notice This function executes a delegate call on a safe if the module is enabled and
* root access it granted.
* @param safe A Safe instance
* @param rootAccess A struct of type SafeRootAccess containing information of about the action to be executed.
* Users can add logic to validate metahash through a transaction guard.
* @return data bytes types containing the result of the executed action.
*/
function executeRootAccess(
ISafe safe,
SafeRootAccess calldata rootAccess
) external override onlyEnabledModule(address(safe)) returns (bytes memory data) {
address safeAddress = address(safe);

SafeProtocolAction calldata safeProtocolAction = rootAccess.action;

if (!ISafeProtocolModule(msg.sender).requiresRootAccess() || !enabledModules[safeAddress][msg.sender].rootAddressGranted) {
revert ModuleRequiresRootAccess(msg.sender);
}

bool success;
(success, data) = safe.execTransactionFromModuleReturnData(
safeProtocolAction.to,
safeProtocolAction.value,
safeProtocolAction.data,
1
);
if (success) {
emit RootAccessActionExecuted(safeAddress, rootAccess.metaHash);
} else {
revert RootAccessActionExecutionFailed(safeAddress, rootAccess.metaHash);
}
}

/**
* @notice Called by a Safe to enable a module on a Safe. To be called by a safe.
* @param module ISafeProtocolModule A module that has to be enabled
* @param allowRootAccess Bool indicating whether root access to be allowed.
*/
function enableModule(address module, bool allowRootAccess) external noZeroOrSentinelModule(module) {
ModuleAccessInfo storage senderSentinelModule = enabledModules[msg.sender][SENTINEL_MODULES];
ModuleAccessInfo storage senderModule = enabledModules[msg.sender][module];

if (senderModule.nextModulePointer != address(0)) {
revert ModuleAlreadyEnabled(msg.sender, module);
}

bool requiresRootAccess = ISafeProtocolModule(module).requiresRootAccess();
if (allowRootAccess != requiresRootAccess) {
revert ModuleAccessMismatch(module, requiresRootAccess, allowRootAccess);
}

if (senderSentinelModule.nextModulePointer == address(0)) {
senderSentinelModule.rootAddressGranted = false;
senderSentinelModule.nextModulePointer = SENTINEL_MODULES;
}

senderModule.nextModulePointer = senderSentinelModule.nextModulePointer;
senderModule.rootAddressGranted = allowRootAccess;
senderSentinelModule.nextModulePointer = module;

emit ModuleEnabled(msg.sender, module, allowRootAccess);
}

/**
* @notice Disable a module. This function should be called by Safe.
* @param module Module to be disabled
*/
function disableModule(address prevModule, address module) external noZeroOrSentinelModule(module) {
ModuleAccessInfo storage prevModuleInfo = enabledModules[msg.sender][prevModule];
ModuleAccessInfo storage moduleInfo = enabledModules[msg.sender][module];

if (prevModuleInfo.nextModulePointer != module) {
revert InvalidPrevModuleAddress(prevModule);
}

prevModuleInfo = moduleInfo;

moduleInfo.nextModulePointer = address(0);
moduleInfo.rootAddressGranted = false;
emit ModuleDisabled(msg.sender, module);
}

/**
* @notice A view only function to get information about safe and a module
* @param safe Address of a safe
* @param module Address of a module
*/
function getModuleInfo(address safe, address module) external view returns (ModuleAccessInfo memory enabled) {
return enabledModules[safe][module];
}

/**
* @notice Returns if an module is enabled
* @return True if the module is enabled
*/
function isModuleEnabled(address safe, address module) public view returns (bool) {
return SENTINEL_MODULES != module && enabledModules[safe][module].nextModulePointer != address(0);
}

/**
* @notice Returns an array of modules enabled for a Safe address.
* If all entries fit into a single page, the next pointer will be 0x1.
* If another page is present, next will be the last element of the returned array.
* @param start Start of the page. Has to be a module or start pointer (0x1 address)
* @param pageSize Maximum number of modules that should be returned. Has to be > 0
* @return array Array of modules.
* @return next Start of the next page.
*/
function getModulesPaginated(
address start,
uint256 pageSize,
address safe
) external view returns (address[] memory array, address next) {
if (pageSize == 0) {
revert ZeroPageSizeNotAllowed();
}

if (!(start == SENTINEL_MODULES || isModuleEnabled(safe, start))) {
revert InvalidModuleAddress(start);
}
// Init array with max page size
array = new address[](pageSize);

// Populate return array
uint256 moduleCount = 0;
next = enabledModules[safe][start].nextModulePointer;
while (next != address(0) && next != SENTINEL_MODULES && moduleCount < pageSize) {
array[moduleCount] = next;
next = enabledModules[safe][next].nextModulePointer;
moduleCount++;
}

// This check is required because the enabled module list might not be initialised yet. e.g. no enabled modules for a safe ever before
if (moduleCount == 0) {
next = SENTINEL_MODULES;
}

/**
Because of the argument validation, we can assume that the loop will always iterate over the valid module list values
and the `next` variable will either be an enabled module or a sentinel address (signalling the end).
If we haven't reached the end inside the loop, we need to set the next pointer to the last element of the modules array
because the `next` variable (which is a module by itself) acting as a pointer to the start of the next page is neither
included to the current page, nor will it be included in the next one if you pass it as a start.
*/
if (next != SENTINEL_MODULES && moduleCount != 0) {
next = array[moduleCount - 1];
}
// Set correct size of returned array
// solhint-disable-next-line no-inline-assembly
assembly {
mstore(array, moduleCount)
}
}
}
4 changes: 2 additions & 2 deletions contracts/SafeProtocolRegistry.sol
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: SEE LICENSE IN LICENSE
pragma solidity ^0.8.18;
import "./interfaces/ISafeProtocolRegistry.sol";
import "@openzeppelin/contracts/access/Ownable2Step.sol";
import {ISafeProtocolRegistry} from "./interfaces/ISafeProtocolRegistry.sol";
import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol";

contract SafeProtocolRegistry is ISafeProtocolRegistry, Ownable2Step {
/**
Expand Down
12 changes: 12 additions & 0 deletions contracts/interfaces/Accounts.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,17 @@ pragma solidity ^0.8.18;
* @title ISafe Declares the functions that are called on a Safe by Safe protocol.
*/
interface ISafe {
function execTransactionFromModule(
address payable to,
uint256 value,
bytes calldata data,
uint8 operation
) external returns (bool success);

function execTransactionFromModuleReturnData(
address to,
uint256 value,
bytes memory data,
uint8 operation
) external returns (bool success, bytes memory returnData);
}
4 changes: 2 additions & 2 deletions contracts/interfaces/Components.sol
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity ^0.8.18;
import "./Accounts.sol";
import "../DataTypes.sol";
import {ISafe} from "./Accounts.sol";
import {SafeTransaction, SafeRootAccess} from "../DataTypes.sol";

/**
* @title ISafeProtocolStaticFallbackMethod - An interface that a Safe fallbackhandler should implement
Expand Down
32 changes: 32 additions & 0 deletions contracts/interfaces/Mediator.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity ^0.8.18;
import {ISafe} from "./Accounts.sol";
import {SafeRootAccess, SafeTransaction} from "../DataTypes.sol";

/**
* @title ISafeProtocolMediator interface a Mediator should implement
* @notice A mediator checks the status of the component through the registry and allows only
* listed and non-flagged components to execute transactions. A Safe account should
* add a mediator as a module.
*/
interface ISafeProtocolMediator {
/**
* @notice This function allows enabled modules to execute non-delegate call transactions thorugh a Safe.
* It should validate the status of the module through the registry and allows only listed and non-flagged components to execute transactions.
* @param safe Address of a Safe account
* @param transaction SafeTransaction instance containing payload information about the transaction
* @return data Array of bytes types returned upon the successful execution of all the actions. The size of the array will be the same as the size of the actions
* in case of succcessful execution. Empty if the call failed.
*/
function executeTransaction(ISafe safe, SafeTransaction calldata transaction) external returns (bytes[] memory data);

/**
* @notice This function allows enabled modules to execute delegate call transactions thorugh a Safe.
* It should validate the status of the module through the registry and allows only listed and non-flagged components to execute transactions.
* @param safe Address of a Safe account
* @param rootAccess SafeTransaction instance containing payload information about the transaction
* @return data Arbitrary length bytes data returned upon the successful execution. The size of the array will be the same as the size of the actions
* in case of succcessful execution. Empty if the call failed.
*/
function executeRootAccess(ISafe safe, SafeRootAccess calldata rootAccess) external returns (bytes memory data);
}
Loading

0 comments on commit 63686c8

Please sign in to comment.