Why can't I use a Gnosis Safe as the crowdsale wallet?

I have a crowdsale contract which inherits the following contracts from openzepplin:

 contract MyCrowdsale is Crowdsale, TimedCrowdsale, AllowanceCrowdsale, PostDeliveryCrowdsale, IncreasingPriceCrowdsale {
        uint256 openingTime,
        uint256 closingTime,
        uint256 initialRate,
        uint256 finalRate,
        address tokenWallet,
        address payable wallet,
        IERC20 token
    Crowdsale(initialRate, wallet, token)
    TimedCrowdsale(openingTime, closingTime)
    IncreasingPriceCrowdsale(initialRate, finalRate)


And everything works fine when I deploy it with the wallet being a Metamask wallet. The wallet is where the collected ETH from the ICO should go.

  await MyCrowdsale.deploy(
     tokenWallet,  //tokenWallet - address holding MyToken to sell (granted allowance)
     myMetamaskVault, //wallet - where to send ETH from crowdsale. This parameter cannot be a Gnosis Safe???

When I deploy the crowdsale with myMetamaskVault as the vault, (approve crowdsale to spend from tokenWallet), everything works great.

However, if I simply replace the vault where to send ETH with a Gnosis multi-sig safe, I can no longer call buyTokens() on the crowdsale. It returns Out of gas, as show in my Tenderly stacktrace:

Screen Shot 2021-07-19 at 9.00.35 AM

I understand that Gnosis wallets cannot be used to sign transactions, but the approve() is the one that needs signing, and that’s for the tokenWallet not the wallet. Why can’t my crowdsale simply transfer received ETH to Gnosis?

Is there a specific limitation in openzepplin Crowdsale that prevents using a multisig safe as the vault?

I suppose I can do this manually, transfering from Metamask to Gnosis as the funds are coming in, or using OZ Autotasks, but this is obviously not ideal.

The crowdsale uses a small amount of gas in the transfer of ETH to the destination wallet, and it is evidently not enough to process a receive in a Gnosis Safe.

You can override the function _forwardFunds to use .call instead of .transfer but you need to make sure to check the return value of .call and revert if it is false.

@frangio Thanks for the response. Is the following all that's needed?

function _forwardFunds() internal {
    (bool success,) = wallet().call.value(msg.value)('');
    require(success, 'Failed to forward funds');

What's the best way to pay for the gas for this transaction? Should I call with no gas() call or somehow estimate it (how), or use a fixed value?

Also, what is the fool-proof safe way to do this? I understand using call can introduce an attack vector. Is a mutex needed on _forwardFunds, even though buyTokens already uses nonReentrant? Is there any other way to improve this code?


Regarding gas, you can leave it implicit.

The nonReentrant modifier in _forwardFunds should be enough unless you extended the crowdsale with other custom external functions, in which case the safest thing would be to have nonReentrant in all of those, if possible.

@frangio When you say " The nonReentrant modifier in _forwardFunds should be enough", do you mean in "buyTokens"? The code I posted does not have the modifier. Should one be added?

Yes, sorry! Meant in buyTokens.