Skip to content

Instantly share code, notes, and snippets.

@rmeissner
Created April 21, 2020 11:34
Show Gist options
  • Save rmeissner/c66371a4b030109cde1020f427122462 to your computer and use it in GitHub Desktop.
Save rmeissner/c66371a4b030109cde1020f427122462 to your computer and use it in GitHub Desktop.
Getting Started with Safe module development

How develop a Safe module

Add dependencies

  • yarn add @gnosis.pm/safe-contracts

  • Optional for testing

    • yarn add eth-lightwallet

Contract setup

Add Imports.sol contract to build Safe sources:

pragma solidity >=0.5.0 <0.6.0;

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

Add Safe imports to module contract:

// Required for triggering execution
import "@gnosis.pm/safe-contracts/contracts/common/Enum.sol";
import "@gnosis.pm/safe-contracts/contracts/GnosisSafe.sol";

contract ExampleModule {
    function tokenTransfer(GnosisSafe safe, address token, address to, uint amount) public {
        bytes memory data = abi.encodeWithSignature("transfer(address,uint256)", to, amount);
        require(safe.execTransactionFromModule(token, 0, data, Enum.Operation.Call), "Could not execute token transfer");
    }
}

Test setup

Setup test artifacts

const GnosisSafe = artifacts.require("./GnosisSafe.sol")
const ProxyFactory = artifacts.require("./ProxyFactory.sol");
const ExampleModule = artifacts.require("./ExampleModule.sol")

Optional: Setup light wallet for signing requests to the Safe (if using off-chain signatures)

contract('ExampleModule', function(accounts) {
    let lw
    
    beforeEach(async function() {
        // Create lightwallet
        lw = await utils.createLightwallet()
        
        // More setup code
    })
    
    // More test code
}

Create master copy and proxy factory

contract('ExampleModule', function(accounts) {
    const ADDRESS_0 = "0x0000000000000000000000000000000000000000"
    
    let gnosisSafeMasterCopy
    let proxyFactory
    
    beforeEach(async function() {
        // Create lightwallet
        lw = await utils.createLightwallet()

        // Create Master Copy
        gnosisSafeMasterCopy = await GnosisSafe.new()
        
        // create Proxy factory
        proxyFactory = await ProxyFactory.new()
    })

    // More test code
}

Optional: Add helper method to execute Safe transction (if using off-chain signatures)

const utils = require('@gnosis.pm/safe-contracts/test/utils/general')
let execTransaction = async function(safe, to, value, data, operation, message) {
    let nonce = await safe.nonce()
    let transactionHash = await safe.getTransactionHash(to, value, data, operation, 0, 0, 0, ADDRESS_0, ADDRESS_0, nonce)
    let sigs = utils.signTransaction(lw, [lw.accounts[0], lw.accounts[1]], transactionHash)
    utils.logGasUsage(
        'execTransaction ' + message,
        await safe.execTransaction(to, value, data, operation, 0, 0, 0, ADDRESS_0, ADDRESS_0, sigs)
    )
}

Create test with Safe and module

contract('ExampleModule', function(accounts) {
    const CALL = 0
    
    it('should execute token transfer', async () => {
        const setupData = await gnosisSafeMasterCopy.setup([lw.accounts[0], lw.accounts[1]], 2, ADDRESS_0, "0x", 0, 0, 0, 0)
        // Find event in tx and create contract instance
        const safe = utils.getParamFromTxEvent(
            await proxyFactory.createProxy(gnosisSafeMasterCopy.address, setupData),
            'ProxyCreation', 'proxy', proxyFactory.address, GnosisSafe, 'create Gnosis Safe' 
        )
        
        // Setup module
        const testModule = await ExampleModule.new()
        
        const enableModuleData = await safe.enableModule(testModule.address).encodeABI()
        await execTransaction(safe.address, 0, enableModuleData, CALL, "enable module")
        
        const modules = await safe.getModules()
        assert.equal(1, modules.length)
        assert.equal(testModule.address, modules[0])
    
        // Setup test token
        const token = await TestToken.new({from: accounts[0]})
        await token.transfer(gnosisSafe.address, 1000, {from: accounts[0]}) 
        
        assert.equal(1000, await token.balanceOf(gnosisSafe.address))
        assert.equal(0, await token.balanceOf(accounts[1]))
        
        // Execute token transfer via module
        await testModule.tokenTransfer(
            safe.address, token.address, accounts[1], 600
        )
        
        assert.equal(400, await token.balanceOf(gnosisSafe.address))
        assert.equal(600, await token.balanceOf(accounts[1]))
    }
}

Notes

TestToken.sol

  • requires yarn add @gnosis.pm/util-contracts
pragma solidity >=0.4.21 <0.6.0;

import "@gnosis.pm/util-contracts/contracts/GnosisStandardToken.sol";

contract TestToken is GnosisStandardToken{
    constructor() public {
        balances[msg.sender] = 10000000;
    }
}

Solidity Copiler >= 0.6.0

It is currently not possible to directly use the Safe contracts with the latest compiler version. In this case it is required to define interfaces for the Safe contracts and use a mock Safe for the tests. Also some of the provided utility methods might not work as expected.

An example can be found at https://gist.github.com/rmeissner/208e35db13d0ebf53b6e35838f1c8122

Sources

The example code is based on https://github.com/rmeissner/transfer-limit

@fastackl
Copy link

fastackl commented Nov 16, 2023

I want to enable a module from a smart contract by calling setUp

The setUp function looks like it's designed to allow this, because it contains a setupModules(address to, bytes memory data) function

I assume to is the address of the proxy. But I'm not sure what to pass into data. I tried passing abi encoded enableModule(address) and the address, but it's not working

any clue how to do this?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment