pragma solidity ^0.4.24;
/**
* @title Module
* @dev Interface for a module.
* A module MUST implement the addModule() method to ensure that a wallet with at least one module
* can never end up in a "frozen" state.
* @author Julien Niset - <julien@argent.xyz>
*/
interface Module {
/**
* @dev Inits a module for a wallet by e.g. setting some wallet specific parameters in storage.
* @param _wallet The wallet.
*/
function init(BaseWallet _wallet) external;
/**
* @dev Adds a module to a wallet.
* @param _wallet The target wallet.
* @param _module The modules to authorise.
*/
function addModule(BaseWallet _wallet, Module _module) external;
/**
* @dev Utility method to recover any ERC20 token that was sent to the
* module by mistake.
* @param _token The token to recover.
*/
function recoverToken(address _token) external;
}
/**
* @title Upgrader
* @dev Interface for a contract that can upgrade wallets by enabling/disabling modules.
* @author Julien Niset - <julien@argent.xyz>
*/
interface Upgrader {
/**
* @dev Upgrades a wallet by enabling/disabling modules.
* @param _wallet The owner.
*/
function upgrade(address _wallet, address[] _toDisable, address[] _toEnable) external;
function toDisable() external view returns (address[]);
function toEnable() external view returns (address[]);
}
/**
* @title BaseModule
* @dev Basic module that contains some methods common to all modules.
* @author Julien Niset - <julien@argent.xyz>
*/
contract BaseModule is Module {
// The adddress of the module registry.
ModuleRegistry internal registry;
event ModuleCreated(bytes32 name);
event ModuleInitialised(address wallet);
constructor(ModuleRegistry _registry, bytes32 _name) public {
registry = _registry;
emit ModuleCreated(_name);
}
/**
* @dev Throws if the sender is not the target wallet of the call.
*/
modifier onlyWallet(BaseWallet _wallet) {
require(msg.sender == address(_wallet), "BM: caller must be wallet");
_;
}
/**
* @dev Throws if the sender is not the owner of the target wallet or the module itself.
*/
modifier onlyOwner(BaseWallet _wallet) {
require(msg.sender == address(this) || isOwner(_wallet, msg.sender), "BM: must be an owner for the wallet");
_;
}
/**
* @dev Throws if the sender is not the owner of the target wallet.
*/
modifier strictOnlyOwner(BaseWallet _wallet) {
require(isOwner(_wallet, msg.sender), "BM: msg.sender must be an owner for the wallet");
_;
}
/**
* @dev Inits the module for a wallet by logging an event.
* The method can only be called by the wallet itself.
* @param _wallet The wallet.
*/
function init(BaseWallet _wallet) external onlyWallet(_wallet) {
emit ModuleInitialised(_wallet);
}
/**
* @dev Adds a module to a wallet. First checks that the module is registered.
* @param _wallet The target wallet.
* @param _module The modules to authorise.
*/
function addModule(BaseWallet _wallet, Module _module) external strictOnlyOwner(_wallet) {
require(registry.isRegisteredModule(_module), "BM: module is not registered");
_wallet.authoriseModule(_module, true);
}
/**
* @dev Utility method enbaling anyone to recover ERC20 token sent to the
* module by mistake and transfer them to the Module Registry.
* @param _token The token to recover.
*/
function recoverToken(address _token) external {
uint total = ERC20(_token).balanceOf(address(this));
ERC20(_token).transfer(address(registry), total);
}
/**
* @dev Helper method to check if an address is the owner of a target wallet.
* @param _wallet The target wallet.
* @param _addr The address.
*/
function isOwner(BaseWallet _wallet, address _addr) internal view returns (bool) {
return _wallet.owner() == _addr;
}
}
/**
* @title RelayerModule
* @dev Base module containing logic to execute transactions signed by eth-less accounts and sent by a relayer.
* @author Julien Niset - <julien@argent.xyz>
*/
contract RelayerModule is Module {
uint256 constant internal BLOCKBOUND = 10000;
mapping (address => RelayerConfig) public relayer;
struct RelayerConfig {
uint256 nonce;
mapping (bytes32 => bool) executedTx;
}
event TransactionExecuted(address indexed wallet, bool indexed success, bytes32 signedHash);
/**
* @dev Throws if the call did not go through the execute() method.
*/
modifier onlyExecute {
require(msg.sender == address(this), "RM: must be called via execute()");
_;
}
/* ***************** Abstract method ************************* */
/**
* @dev Gets the number of valid signatures that must be provided to execute a
* specific relayed transaction.
* @param _wallet The target wallet.
* @param _data The data of the relayed transaction.
* @return The number of required signatures.
*/
function getRequiredSignatures(BaseWallet _wallet, bytes _data) internal view returns (uint256);
/**
* @dev Validates the signatures provided with a relayed transaction.
* The method MUST throw if one or more signatures are not valid.
* @param _wallet The target wallet.
* @param _data The data of the relayed transaction.
* @param _signHash The signed hash representing the relayed transaction.
* @param _signatures The signatures as a concatenated byte array.
*/
function validateSignatures(BaseWallet _wallet, bytes _data, bytes32 _signHash, bytes _signatures) internal view returns (bool);
/* ************************************************************ */
/**
* @dev Executes a relayed transaction.
* @param _wallet The target wallet.
* @param _data The data for the relayed transaction
* @param _nonce The nonce used to prevent replay attacks.
* @param _signatures The signatures as a concatenated byte array.
* @param _gasPrice The gas price to use for the gas refund.
* @param _gasLimit The gas limit to use for the gas refund.
*/
function execute(
BaseWallet _wallet,
bytes _data,
uint256 _nonce,
bytes _signatures,
uint256 _gasPrice,
uint256 _gasLimit
)
external
returns (bool success)
{
uint startGas = gasleft();
bytes32 signHash = getSignHash(address(this), _wallet, 0, _data, _nonce, _gasPrice, _gasLimit);
require(checkAndUpdateUniqueness(_wallet, _nonce, signHash), "RM: Duplicate request");
require(verifyData(address(_wallet), _data), "RM: the wallet authorized is different then the target of the relayed data");
uint256 requiredSignatures = getRequiredSignatures(_wallet, _data);
if((requiredSignatures * 65) == _signatures.length) {
if(verifyRefund(_wallet, _gasLimit, _gasPrice, requiredSignatures)) {
if(requiredSignatures == 0 || validateSignatures(_wallet, _data, signHash, _signatures)) {
// solium-disable-next-line security/no-call-value
success = address(this).call(_data);
refund(_wallet, startGas - gasleft(), _gasPrice, _gasLimit, requiredSignatures, msg.sender);
}
}
}
emit TransactionExecuted(_wallet, success, signHash);
}
/**
* @dev Gets the current nonce for a wallet.
* @param _wallet The target wallet.
*/
function getNonce(BaseWallet _wallet) external view returns (uint256 nonce) {
return relayer[_wallet].nonce;
}
/**
* @dev Generates the signed hash of a relayed transaction according to ERC 1077.
* @param _from The starting address for the relayed transaction (should be the module)
* @param _to The destination address for the relayed transaction (should be the wallet)
* @param _value The value for the relayed transaction
* @param _data The data for the relayed transaction
* @param _nonce The nonce used to prevent replay attacks.
* @param _gasPrice The gas price to use for the gas refund.
* @param _gasLimit The gas limit to use for the gas refund.
*/
function getSignHash(
address _from,
address _to,
uint256 _value,
bytes _data,
uint256 _nonce,
uint256 _gasPrice,
uint256 _gasLimit
)
internal
pure
returns (bytes32)
{
return keccak256(
abi.encodePacked(
"\x19Ethereum Signed Message:\n32",
keccak256(abi.encodePacked(byte(0x19), byte(0), _from, _to, _value, _data, _nonce, _gasPrice, _gasLimit))
));
}
/**
* @dev Checks if the relayed transaction is unique.
* @param _wallet The target wallet.
* @param _nonce The nonce
* @param _signHash The signed hash of the transaction
*/
function checkAndUpdateUniqueness(BaseWallet _wallet, uint256 _nonce, bytes32 _signHash) internal returns (bool) {
if(relayer[_wallet].executedTx[_signHash] == true) {
return false;
}
relayer[_wallet].executedTx[_signHash] = true;
return true;
}
/**
* @dev Checks that a nonce has the correct format and is valid.
* It must be constructed as nonce = {block number}{timestamp} where each component is 16 bytes.
* @param _wallet The target wallet.
* @param _nonce The nonce
*/
function checkAndUpdateNonce(BaseWallet _wallet, uint256 _nonce) internal returns (bool) {
if(_nonce <= relayer[_wallet].nonce) {
return false;
}
uint256 nonceBlock = (_nonce & 0xffffffffffffffffffffffffffffffff00000000000000000000000000000000) >> 128;
if(nonceBlock > block.number + BLOCKBOUND) {
return false;
}
relayer[_wallet].nonce = _nonce;
return true;
}
/**
* @dev Recovers the signer at a given position from a list of concatenated signatures.
* @param _signedHash The signed hash
* @param _signatures The concatenated signatures.
* @param _index The index of the signature to recover.
*/
function recoverSigner(bytes32 _signedHash, bytes _signatures, uint _index) internal pure returns (address) {
uint8 v;
bytes32 r;
bytes32 s;
// we jump 32 (0x20) as the first slot of bytes contains the length
// we jump 65 (0x41) per signature
// for v we load 32 bytes ending with v (the first 31 come from s) then apply a mask
// solium-disable-next-line security/no-inline-assembly
assembly {
r := mload(add(_signatures, add(0x20,mul(0x41,_index))))
s := mload(add(_signatures, add(0x40,mul(0x41,_index))))
v := and(mload(add(_signatures, add(0x41,mul(0x41,_index)))), 0xff)
}
require(v == 27 || v == 28);
return ecrecover(_signedHash, v, r, s);
}
/**
* @dev Refunds the gas used to the Relayer.
* For security reasons the default behavior is to not refund calls with 0 or 1 signatures.
* @param _wallet The target wallet.
* @param _gasUsed The gas used.
* @param _gasPrice The gas price for the refund.
* @param _gasLimit The gas limit for the refund.
* @param _signatures The number of signatures used in the call.
* @param _relayer The address of the Relayer.
*/
function refund(BaseWallet _wallet, uint _gasUsed, uint _gasPrice, uint _gasLimit, uint _signatures, address _relayer) internal {
uint256 amount = 29292 + _gasUsed; // 21000 (transaction) + 7620 (execution of refund) + 672 to log the event + _gasUsed
// only refund if gas price not null, more than 1 signatures, gas less than gasLimit
if(_gasPrice > 0 && _signatures > 1 && amount <= _gasLimit) {
if(_gasPrice > tx.gasprice) {
amount = amount * tx.gasprice;
}
else {
amount = amount * _gasPrice;
}
_wallet.invoke(_relayer, amount, "");
}
}
/**
* @dev Returns false if the refund is expected to fail.
* @param _wallet The target wallet.
* @param _gasUsed The expected gas used.
* @param _gasPrice The expected gas price for the refund.
*/
function verifyRefund(BaseWallet _wallet, uint _gasUsed, uint _gasPrice, uint _signatures) internal view returns (bool) {
if(_gasPrice > 0
&& _signatures > 1
&& (address(_wallet).balance < _gasUsed * _gasPrice || _wallet.authorised(this) == false)) {
return false;
}
return true;
}
/**
* @dev Checks that the wallet address provided as the first parameter of the relayed data is the same
* as the wallet passed as the input of the execute() method.
@return false if the addresses are different.
*/
function verifyData(address _wallet, bytes _data) private pure returns (bool) {
require(_data.length >= 36, "RM: Invalid dataWallet");
address dataWallet;
// solium-disable-next-line security/no-inline-assembly
assembly {
//_data = {length:32}{sig:4}{_wallet:32}{...}
dataWallet := mload(add(_data, 0x24))
}
return dataWallet == _wallet;
}
/**
* @dev Parses the data to extract the method signature.
*/
function functionPrefix(bytes _data) internal pure returns (bytes4 prefix) {
require(_data.length >= 4, "RM: Invalid functionPrefix");
// solium-disable-next-line security/no-inline-assembly
assembly {
prefix := mload(add(_data, 0x20))
}
}
}
/**
* @title OnlyOwnerModule
* @dev Module that extends BaseModule and RelayerModule for modules where the execute() method
* must be called with one signature frm the owner.
* @author Julien Niset - <julien@argent.xyz>
*/
contract OnlyOwnerModule is BaseModule, RelayerModule {
// *************** Implementation of RelayerModule methods ********************* //
// Overrides to use the incremental nonce and save some gas
function checkAndUpdateUniqueness(BaseWallet _wallet, uint256 _nonce, bytes32 _signHash) internal returns (bool) {
return checkAndUpdateNonce(_wallet, _nonce);
}
function validateSignatures(BaseWallet _wallet, bytes _data, bytes32 _signHash, bytes _signatures) internal view returns (bool) {
address signer = recoverSigner(_signHash, _signatures, 0);
return isOwner(_wallet, signer); // "OOM: signer must be owner"
}
function getRequiredSignatures(BaseWallet _wallet, bytes _data) internal view returns (uint256) {
return 1;
}
}
/**
* ERC20 contract interface.
*/
contract ERC20 {
function totalSupply() public view returns (uint);
function decimals() public view returns (uint);
function balanceOf(address tokenOwner) public view returns (uint balance);
function allowance(address tokenOwner, address spender) public view returns (uint remaining);
function transfer(address to, uint tokens) public returns (bool success);
function approve(address spender, uint tokens) public returns (bool success);
function transferFrom(address from, address to, uint tokens) public returns (bool success);
}
/**
* @title Owned
* @dev Basic contract to define an owner.
* @author Julien Niset - <julien@argent.xyz>
*/
contract Owned {
// The owner
address public owner;
event OwnerChanged(address indexed _newOwner);
/**
* @dev Throws if the sender is not the owner.
*/
modifier onlyOwner {
require(msg.sender == owner, "Must be owner");
_;
}
constructor() public {
owner = msg.sender;
}
/**
* @dev Lets the owner transfer ownership of the contract to a new owner.
* @param _newOwner The new owner.
*/
function changeOwner(address _newOwner) external onlyOwner {
require(_newOwner != address(0), "Address must not be null");
owner = _newOwner;
emit OwnerChanged(_newOwner);
}
}
/**
* @title ModuleRegistry
* @dev Registry of authorised modules.
* Modules must be registered before they can be authorised on a wallet.
* @author Julien Niset - <julien@argent.xyz>
*/
contract ModuleRegistry is Owned {
mapping (address => Info) internal modules;
mapping (address => Info) internal upgraders;
event ModuleRegistered(address indexed module, bytes32 name);
event ModuleDeRegistered(address module);
event UpgraderRegistered(address indexed upgrader, bytes32 name);
event UpgraderDeRegistered(address upgrader);
struct Info {
bool exists;
bytes32 name;
}
/**
* @dev Registers a module.
* @param _module The module.
* @param _name The unique name of the module.
*/
function registerModule(address _module, bytes32 _name) external onlyOwner {
require(!modules[_module].exists, "MR: module already exists");
modules[_module] = Info({exists: true, name: _name});
emit ModuleRegistered(_module, _name);
}
/**
* @dev Deregisters a module.
* @param _module The module.
*/
function deregisterModule(address _module) external onlyOwner {
require(modules[_module].exists, "MR: module does not exists");
delete modules[_module];
emit ModuleDeRegistered(_module);
}
/**
* @dev Registers an upgrader.
* @param _upgrader The upgrader.
* @param _name The unique name of the upgrader.
*/
function registerUpgrader(address _upgrader, bytes32 _name) external onlyOwner {
require(!upgraders[_upgrader].exists, "MR: upgrader already exists");
upgraders[_upgrader] = Info({exists: true, name: _name});
emit UpgraderRegistered(_upgrader, _name);
}
/**
* @dev Deregisters an upgrader.
* @param _upgrader The _upgrader.
*/
function deregisterUpgrader(address _upgrader) external onlyOwner {
require(upgraders[_upgrader].exists, "MR: upgrader does not exists");
delete upgraders[_upgrader];
emit UpgraderDeRegistered(_upgrader);
}
/**
* @dev Utility method enbaling the owner of the registry to claim any ERC20 token that was sent to the
* registry.
* @param _token The token to recover.
*/
function recoverToken(address _token) external onlyOwner {
uint total = ERC20(_token).balanceOf(address(this));
ERC20(_token).transfer(msg.sender, total);
}
/**
* @dev Gets the name of a module from its address.
* @param _module The module address.
* @return the name.
*/
function moduleInfo(address _module) external view returns (bytes32) {
return modules[_module].name;
}
/**
* @dev Gets the name of an upgrader from its address.
* @param _upgrader The upgrader address.
* @return the name.
*/
function upgraderInfo(address _upgrader) external view returns (bytes32) {
return upgraders[_upgrader].name;
}
/**
* @dev Checks if a module is registered.
* @param _module The module address.
* @return true if the module is registered.
*/
function isRegisteredModule(address _module) external view returns (bool) {
return modules[_module].exists;
}
/**
* @dev Checks if a list of modules are registered.
* @param _modules The list of modules address.
* @return true if all the modules are registered.
*/
function isRegisteredModule(address[] _modules) external view returns (bool) {
for(uint i = 0; i < _modules.length; i++) {
if (!modules[_modules[i]].exists) {
return false;
}
}
return true;
}
/**
* @dev Checks if an upgrader is registered.
* @param _upgrader The upgrader address.
* @return true if the upgrader is registered.
*/
function isRegisteredUpgrader(address _upgrader) external view returns (bool) {
return upgraders[_upgrader].exists;
}
}
/**
* @title BaseWallet
* @dev Simple modular wallet that authorises modules to call its invoke() method.
* Based on https://gist.github.com/Arachnid/a619d31f6d32757a4328a428286da186 by
* @author Julien Niset - <julien@argent.xyz>
*/
contract BaseWallet {
// The implementation of the proxy
address public implementation;
// The owner
address public owner;
// The authorised modules
mapping (address => bool) public authorised;
// The enabled static calls
mapping (bytes4 => address) public enabled;
// The number of modules
uint public modules;
event AuthorisedModule(address indexed module, bool value);
event EnabledStaticCall(address indexed module, bytes4 indexed method);
event Invoked(address indexed module, address indexed target, uint indexed value, bytes data);
event Received(uint indexed value, address indexed sender, bytes data);
event OwnerChanged(address owner);
/**
* @dev Throws if the sender is not an authorised module.
*/
modifier moduleOnly {
require(authorised[msg.sender], "BW: msg.sender not an authorized module");
_;
}
/**
* @dev Inits the wallet by setting the owner and authorising a list of modules.
* @param _owner The owner.
* @param _modules The modules to authorise.
*/
function init(address _owner, address[] _modules) external {
require(owner == address(0) && modules == 0, "BW: wallet already initialised");
require(_modules.length > 0, "BW: construction requires at least 1 module");
owner = _owner;
modules = _modules.length;
for(uint256 i = 0; i < _modules.length; i++) {
require(authorised[_modules[i]] == false, "BW: module is already added");
authorised[_modules[i]] = true;
Module(_modules[i]).init(this);
emit AuthorisedModule(_modules[i], true);
}
}
/**
* @dev Enables/Disables a module.
* @param _module The target module.
* @param _value Set to true to authorise the module.
*/
function authoriseModule(address _module, bool _value) external moduleOnly {
if (authorised[_module] != _value) {
if(_value == true) {
modules += 1;
authorised[_module] = true;
Module(_module).init(this);
}
else {
modules -= 1;
require(modules > 0, "BW: wallet must have at least one module");
delete authorised[_module];
}
emit AuthorisedModule(_module, _value);
}
}
/**
* @dev Enables a static method by specifying the target module to which the call
* must be delegated.
* @param _module The target module.
* @param _method The static method signature.
*/
function enableStaticCall(address _module, bytes4 _method) external moduleOnly {
require(authorised[_module], "BW: must be an authorised module for static call");
enabled[_method] = _module;
emit EnabledStaticCall(_module, _method);
}
/**
* @dev Sets a new owner for the wallet.
* @param _newOwner The new owner.
*/
function setOwner(address _newOwner) external moduleOnly {
require(_newOwner != address(0), "BW: address cannot be null");
owner = _newOwner;
emit OwnerChanged(_newOwner);
}
/**
* @dev Performs a generic transaction.
* @param _target The address for the transaction.
* @param _value The value of the transaction.
* @param _data The data of the transaction.
*/
function invoke(address _target, uint _value, bytes _data) external moduleOnly {
// solium-disable-next-line security/no-call-value
require(_target.call.value(_value)(_data), "BW: call to target failed");
emit Invoked(msg.sender, _target, _value, _data);
}
/**
* @dev This method makes it possible for the wallet to comply to interfaces expecting the wallet to
* implement specific static methods. It delegates the static call to a target contract if the data corresponds
* to an enabled method, or logs the call otherwise.
*/
function() public payable {
if(msg.data.length > 0) {
address module = enabled[msg.sig];
if(module == address(0)) {
emit Received(msg.value, msg.sender, msg.data);
}
else {
require(authorised[module], "BW: must be an authorised module for static call");
// solium-disable-next-line security/no-inline-assembly
assembly {
calldatacopy(0, 0, calldatasize())
let result := staticcall(gas, module, 0, calldatasize(), 0, 0)
returndatacopy(0, 0, returndatasize())
switch result
case 0 {revert(0, returndatasize())}
default {return (0, returndatasize())}
}
}
}
}
}
/**
* @title ModuleManager
* @dev Module to manage the addition, removal and upgrade of the modules of wallets.
* @author Julien Niset - <julien@argent.xyz>
*/
contract ModuleManager is BaseModule, RelayerModule, OnlyOwnerModule {
bytes32 constant NAME = "ModuleManager";
constructor(ModuleRegistry _registry) BaseModule(_registry, NAME) public {
}
/**
* @dev Upgrades the modules of a wallet.
* The implementation of the upgrade is delegated to a contract implementing the Upgrade interface.
* This makes it possible for the manager to implement any possible present and future upgrades
* without the need to authorise modules just for the upgrade process.
* @param _wallet The target wallet.
* @param _upgrader The address of an implementation of the Upgrader interface.
*/
function upgrade(BaseWallet _wallet, Upgrader _upgrader) external onlyOwner(_wallet) {
require(registry.isRegisteredUpgrader(_upgrader), "MM: upgrader is not registered");
address[] memory toDisable = _upgrader.toDisable();
address[] memory toEnable = _upgrader.toEnable();
bytes memory methodData = abi.encodeWithSignature("upgrade(address,address[],address[])", _wallet, toDisable, toEnable);
// solium-disable-next-line security/no-low-level-calls
require(address(_upgrader).delegatecall(methodData), "MM: upgrade failed");
}
}
{
"compilationTarget": {
"ModuleManager.sol": "ModuleManager"
},
"evmVersion": "byzantium",
"libraries": {},
"optimizer": {
"enabled": true,
"runs": 999
},
"remappings": []
}
[{"constant":false,"inputs":[{"name":"_wallet","type":"address"}],"name":"init","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"_wallet","type":"address"}],"name":"getNonce","outputs":[{"name":"nonce","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_wallet","type":"address"},{"name":"_module","type":"address"}],"name":"addModule","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_wallet","type":"address"},{"name":"_upgrader","type":"address"}],"name":"upgrade","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_token","type":"address"}],"name":"recoverToken","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_wallet","type":"address"},{"name":"_data","type":"bytes"},{"name":"_nonce","type":"uint256"},{"name":"_signatures","type":"bytes"},{"name":"_gasPrice","type":"uint256"},{"name":"_gasLimit","type":"uint256"}],"name":"execute","outputs":[{"name":"success","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"","type":"address"}],"name":"relayer","outputs":[{"name":"nonce","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"inputs":[{"name":"_registry","type":"address"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"name":"wallet","type":"address"},{"indexed":true,"name":"success","type":"bool"},{"indexed":false,"name":"signedHash","type":"bytes32"}],"name":"TransactionExecuted","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"name","type":"bytes32"}],"name":"ModuleCreated","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"wallet","type":"address"}],"name":"ModuleInitialised","type":"event"}]