diff --git a/contracts/Migrations.sol b/contracts/Migrations.sol deleted file mode 100644 index 9ffe9a1d..00000000 --- a/contracts/Migrations.sol +++ /dev/null @@ -1,31 +0,0 @@ -pragma solidity >=0.5.0 <0.7.0; - -contract Migrations { - address public owner; - uint public last_completed_migration; - - modifier restricted() { - if (msg.sender == owner) _; - } - - constructor() - public - { - owner = msg.sender; - } - - function setCompleted(uint completed) - public - restricted - { - last_completed_migration = completed; - } - - function upgrade(address new_address) - public - restricted - { - Migrations upgraded = Migrations(new_address); - upgraded.setCompleted(last_completed_migration); - } -} diff --git a/contracts/CPKFactory.sol b/contracts/solc-0.5/CPKFactoryV1.sol similarity index 98% rename from contracts/CPKFactory.sol rename to contracts/solc-0.5/CPKFactoryV1.sol index b83cf011..3c0e9656 100644 --- a/contracts/CPKFactory.sol +++ b/contracts/solc-0.5/CPKFactoryV1.sol @@ -4,7 +4,7 @@ import { Enum } from "@gnosis.pm/safe-contracts/contracts/common/Enum.sol"; import { GnosisSafeProxy } from "@gnosis.pm/safe-contracts/contracts/proxies/GnosisSafeProxy.sol"; import { GnosisSafe } from "@gnosis.pm/safe-contracts/contracts/GnosisSafe.sol"; -contract CPKFactory { +contract CPKFactoryV1 { event ProxyCreation(GnosisSafeProxy proxy); function proxyCreationCode() external pure returns (bytes memory) { diff --git a/contracts/Deps.sol b/contracts/solc-0.5/Deps.sol similarity index 100% rename from contracts/Deps.sol rename to contracts/solc-0.5/Deps.sol diff --git a/contracts/solc-0.8/CPKFactory.sol b/contracts/solc-0.8/CPKFactory.sol new file mode 100644 index 00000000..68878950 --- /dev/null +++ b/contracts/solc-0.8/CPKFactory.sol @@ -0,0 +1,109 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity >=0.8.0; + +import { IGnosisSafeProxyFactory } from "./dep-ports/IGnosisSafeProxyFactory.sol"; +import { ProxyImplSetter } from "./ProxyImplSetter.sol"; +import { SafeSignatureUtils } from "./SafeSignatureUtils.sol"; + +enum TxReaction { + RevertOnReturnFalse, + CaptureBoolReturn, + IgnoreReturn +} + +struct CPKFactoryTx { + uint value; + bytes data; + TxReaction reaction; +} + +contract CPKFactory { + using SafeSignatureUtils for bytes; + + event CPKCreation( + address indexed proxy, + address initialImpl, + address initialOwner, + uint256 salt + ); + + uint256 public constant version = 2; + ProxyImplSetter public immutable proxyImplSetter; + IGnosisSafeProxyFactory public immutable gnosisSafeProxyFactory; + bytes32 public immutable proxyExtCodeHash; + + constructor(IGnosisSafeProxyFactory _gnosisSafeProxyFactory) { + proxyImplSetter = new ProxyImplSetter(address(this)); + gnosisSafeProxyFactory = _gnosisSafeProxyFactory; + proxyExtCodeHash = keccak256(_gnosisSafeProxyFactory.proxyRuntimeCode()); + } + + function proxyCreationCode() external view returns (bytes memory) { + return gnosisSafeProxyFactory.proxyCreationCode(); + } + + function proxyRuntimeCode() external view returns (bytes memory) { + return gnosisSafeProxyFactory.proxyRuntimeCode(); + } + + function createProxyAndExecTransactions( + address owner, + address safeVersion, + uint256 salt, + CPKFactoryTx[] calldata txs, + bytes calldata signature + ) + external + payable + returns (bool execTransactionSuccess) + { + bytes memory data = abi.encode(safeVersion, salt, txs); + bytes32 dataHash = keccak256(data); + signature.check(dataHash, data, owner); + + bytes32 saltNonce = keccak256(abi.encode(owner, salt)); + + address payable proxy = gnosisSafeProxyFactory.createProxyWithNonce( + address(proxyImplSetter), + "", + uint256(saltNonce) + ); + + ProxyImplSetter(proxy).setImplementation(safeVersion); + + uint sumTxsValues = 0; + for (uint i = 0; i < txs.length; i++) { + bool txSuccess; + bytes memory returnData; + uint txValue = txs[i].value; + sumTxsValues += txValue; + (txSuccess, returnData) = proxy.call{value: txValue}(txs[i].data); + assembly { + // txSuccess == 0 means the call failed + if iszero(txSuccess) { + // The revert data begins one word after the returnData pointer. + // At the location returnData in memory, the length of the bytes is stored. + // This differs from the high-level revert(string(returnData)) + // as the high-level version encodes the returnData in a Error(string) object. + // We want to avoid that because the underlying call should have already + // formatted the data in an Error(string) object + revert(add(0x20, returnData), mload(returnData)) + } + } + + TxReaction txReaction = txs[i].reaction; + if (txReaction == TxReaction.RevertOnReturnFalse) { + bool success = abi.decode(returnData, (bool)); + require(success, "tx returned boolean indicating internal failure"); + } else if (txReaction == TxReaction.CaptureBoolReturn) { + execTransactionSuccess = abi.decode(returnData, (bool)); + } // else txReaction assumed to be IgnoreReturn, which does nothing else here + } + + // it is up to the caller to make sure that the msg.value of this method + // equals the sum of all the values in the txs + require(msg.value == sumTxsValues, "msg.value must equal sum of txs' values"); + + emit CPKCreation(proxy, safeVersion, owner, salt); + } +} diff --git a/contracts/solc-0.8/CPKFactoryFacade.sol b/contracts/solc-0.8/CPKFactoryFacade.sol new file mode 100644 index 00000000..0e299ede --- /dev/null +++ b/contracts/solc-0.8/CPKFactoryFacade.sol @@ -0,0 +1,148 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity >=0.8.0; + +import { Enum } from "./dep-ports/Enum.sol"; +import { CPKFactory, CPKFactoryTx, TxReaction } from "./CPKFactory.sol"; +import { SafeSignatureUtils } from "./SafeSignatureUtils.sol"; + +contract CPKFactoryFacade { + using SafeSignatureUtils for bytes; + + CPKFactory immutable cpkFactory; + address immutable safeVersion; + uint256 immutable salt; + address immutable fallbackHandler; + + constructor( + CPKFactory _cpkFactory, + address _safeVersion, + uint256 _salt, + address _fallbackHandler + ) { + cpkFactory = _cpkFactory; + safeVersion = _safeVersion; + salt = _salt; + fallbackHandler = _fallbackHandler; + } + + function execTransaction( + address /* to */, + uint256 /* value */, + bytes calldata /* data */, + Enum.Operation /* operation */, + uint256 /* safeTxGas */, + uint256 /* baseGas */, + uint256 /* gasPrice */, + address /* gasToken */, + address payable /* refundReceiver */, + bytes calldata signatures + ) + external + payable + returns (bool) + { + // The signatures here aren't just the Gnosis Safe signatures, + // but more data has been appended to them. In particular, the + // format for the signatures is now like the following: + // [inner sig][owner][outer sig] + // where the inner signature is what is submitted to the + // proxy as a first transaction to be executed after its creation, + // the owner is the address of the owner of the new proxy, + // left-padded to 32 bytes, and the outer signature is the overall + // signature required by the CPKFactory. + // The following process copies this signatures data into memory, + // and then figures out the length of the inner signature, + // and then extracts the owner from the signatures in memory, + // and then overwrites the owner in memory with the length + // of the outer signature, which is just the remaining + // bytes of the signature. We end up with a situation in memory like + // [inner sig]*[outer sig length][outer sig] + // where the * is the pointer assigned to outerSig. + bytes memory outerSig; + address owner; + uint innerSigLen; + { + bytes memory innerSig = signatures; + innerSigLen = innerSig.actualLength(); + uint outerSigLen = signatures.length - innerSigLen - 0x20; + assembly { + outerSig := add(add(innerSig, 0x20), innerSigLen) + owner := mload(outerSig) + mstore(outerSig, outerSigLen) + } + } + + CPKFactoryTx[] memory txs = new CPKFactoryTx[](2); + { + address[] memory owners = new address[](1); + owners[0] = address(owner); + txs[0] = CPKFactoryTx({ + value: 0, + data: abi.encodeWithSignature( + "setup(" + "address[]," // owners + "uint256," // threshold + "address," // to + "bytes," // data + "address," // fallbackHandler + "address," // paymentToken + "uint256," // payment + "address" // paymentReceiver + ")", + owners, uint256(1), address(0), "", + fallbackHandler, address(0), uint256(0), payable(0) + ), + reaction: TxReaction.IgnoreReturn + }); + } + + { + // trying to reencode the msg.data with the params except instead + // of signatures using innerSig would be the obvious and readable approach + // except we run into stack too deep errors that can't be circumvented yet, + // so instead we copy the entire msg.data into memory and + // mutate the signatures portion in it. + bytes memory innerTxData = msg.data; + assembly { + // index param 9 is signatures, and 9 * 0x20 + 4 = 0x124 + // 0x124 from the start of the data portion of innerTxData + // contains the offset to the start of the word + // containing a further offset of where the signatures data + // begins. + // We add 0x20 to account for the word containing the length + // of innerTxData, making a total offset of 0x144 to the offset data + // Then we add that offset, a word for the overall length, and + // the pointer to innerTxData, to arrive at a pointer to the signatures + // inside innerTxData + let sigs := add(innerTxData, add(mload(add(innerTxData, 0x144)), 0x20)) + // Since we know the length of the inner signature, we get the + // size reduction we need by subtracting the inner signature length + // from the size of all the signature data + let sizeReduction := sub(mload(sigs), innerSigLen) + mstore(sigs, innerSigLen) + // The size reduction is then used to cut the outer signature out + // of the msg.data. + // This assumes that the signatures data is the last chunk of data + // in the msg.data. + // We can't keep the outer signature in the inner transaction data + // because the outer signature signs the inner transaction data, + // so keeping that there would make the outer signature impossible + // to generate. + mstore(innerTxData, sub(mload(innerTxData), sizeReduction)) + } + txs[1] = CPKFactoryTx({ + value: msg.value, + data: innerTxData, + reaction: TxReaction.CaptureBoolReturn + }); + } + + return cpkFactory.createProxyAndExecTransactions{value: msg.value}( + owner, + safeVersion, + salt, + txs, + outerSig + ); + } +} \ No newline at end of file diff --git a/contracts/Multistep.sol b/contracts/solc-0.8/Multistep.sol similarity index 89% rename from contracts/Multistep.sol rename to contracts/solc-0.8/Multistep.sol index 57ae47f4..afba2d16 100644 --- a/contracts/Multistep.sol +++ b/contracts/solc-0.8/Multistep.sol @@ -1,6 +1,7 @@ -pragma solidity >=0.5.0 <0.7.0; +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity >=0.8.0; -import { IERC20 } from "openzeppelin-solidity/contracts/token/ERC20/IERC20.sol"; +import { IERC20 } from "./dep-ports/IERC20.sol"; contract Multistep { event ExpensiveHashMade(bytes32 h); @@ -35,7 +36,7 @@ contract Multistep { require(lastStepFinished[msg.sender] + 1 == step, "must do the next ether step"); require(msg.value >= step * 1 ether, "must provide right amount of ether"); lastStepFinished[msg.sender]++; - msg.sender.transfer(msg.value - step * 1 ether); + payable(msg.sender).transfer(msg.value - step * 1 ether); makeExpensiveHash(step); } diff --git a/contracts/solc-0.8/ProxyImplSetter.sol b/contracts/solc-0.8/ProxyImplSetter.sol new file mode 100644 index 00000000..83112639 --- /dev/null +++ b/contracts/solc-0.8/ProxyImplSetter.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity >=0.8.0; + +contract ProxyImplSetter { + address public immutable initialSetter; + + address implementation; + + constructor(address _initialSetter) { + initialSetter = _initialSetter; + } + + function setImplementation(address _implementation) external { + require(msg.sender == initialSetter, "Implementation must be set by designated initial setter"); + implementation = _implementation; + } +} diff --git a/contracts/solc-0.8/SafeSignatureUtils.sol b/contracts/solc-0.8/SafeSignatureUtils.sol new file mode 100644 index 00000000..5b148eb0 --- /dev/null +++ b/contracts/solc-0.8/SafeSignatureUtils.sol @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity >=0.8.0; + +import { ISignatureValidator, EIP1271_MAGIC_VALUE } from "./dep-ports/ISignatureValidator.sol"; + +library SafeSignatureUtils { + // Adapted from SignatureDecoder + function components(bytes memory signature) + internal + pure + returns (uint8 v, bytes32 r, bytes32 s) + { + // The signature format is a compact form of: + // {bytes32 r}{bytes32 s}{uint8 v} + // Compact means, uint8 is not padded to 32 bytes. + // solium-disable-next-line security/no-inline-assembly + assembly { + r := mload(add(signature, 0x20)) + s := mload(add(signature, 0x40)) + // Here we are loading the last 32 bytes, including 31 bytes + // of 's'. There is no 'mload8' to do this. + // + // 'byte' is not working due to the Solidity parser, so lets + // use the second best option, 'and' + v := and(mload(add(signature, 0x41)), 0xff) + } + } + + // Adapted from GnosisSafe + function check(bytes memory signature, bytes32 dataHash, bytes memory data, address owner) + internal + view + { + // Check that the provided signature data is not too short + require(signature.length >= 65, "Signature data too short"); + + uint8 v; + bytes32 r; + bytes32 s; + (v, r, s) = components(signature); + + address derivedOwner; + // If v is 0 then it is a contract signature + if (v == 0) { + // When handling contract signatures the address of the contract is encoded into r + derivedOwner = address(uint160(uint256(r))); + + uint256 contractSignatureLen = requireContractSignatureLength(signature, uint256(s)); + require(uint256(s) + 32 + contractSignatureLen <= signature.length, "Invalid contract signature location: data not complete"); + + // Check signature + bytes memory contractSignature; + // solium-disable-next-line security/no-inline-assembly + assembly { + // The signature data for contract signatures is appended to the concatenated signatures and the offset is stored in s + contractSignature := add(add(signature, s), 0x20) + } + require(ISignatureValidator(derivedOwner).isValidSignature(data, contractSignature) == EIP1271_MAGIC_VALUE, "Invalid contract signature provided"); + // If v is 1 then it is an approved hash + } else if (v == 1) { + // When handling approved hashes the address of the approver is encoded into r + derivedOwner = address(uint160(uint256(r))); + // Hashes are automatically approved by the sender of the message or when they have been pre-approved via a separate transaction + require(msg.sender == derivedOwner); + } else if (v > 30) { + // To support eth_sign and similar we adjust v and hash the messageHash with the Ethereum message prefix before applying ecrecover + derivedOwner = ecrecover(keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", dataHash)), v - 4, r, s); + } else { + // Use ecrecover with the messageHash for EOA signatures + derivedOwner = ecrecover(dataHash, v, r, s); + } + require (derivedOwner == owner, "Invalid owner provided"); + } + + function requireContractSignatureLength( + bytes memory signature, + uint256 sigLoc + ) + internal + pure + returns (uint256 contractSignatureLen) + { + // Check that signature data pointer (s) is not pointing inside the static part of the signatures bytes + // This check is not completely accurate, since it is possible that more signatures than the threshold are send. + // Here we only check that the pointer is not pointing inside the part that is being processed + require(sigLoc >= 65, "Invalid contract signature location: inside static part"); + + // Check that signature data pointer (s) is in bounds (points to the length of data -> 32 bytes) + require(sigLoc + 32 <= signature.length, "Invalid contract signature location: length not present"); + + // Check if the contract signature is in bounds: start of data is s + 32 and end is start + signature length + // solium-disable-next-line security/no-inline-assembly + assembly { + contractSignatureLen := mload(add(add(signature, sigLoc), 0x20)) + } + } + + function actualLength(bytes memory signature) + internal + pure + returns (uint256) + { + uint8 v; + bytes32 r; + bytes32 s; + (v, r, s) = components(signature); + + if (v == 0) { + uint256 contractSignatureLen = requireContractSignatureLength(signature, uint256(s)); + return uint256(s) + 32 + contractSignatureLen; + } + + return 0x41; + } +} \ No newline at end of file diff --git a/contracts/solc-0.8/dep-ports/Enum.sol b/contracts/solc-0.8/dep-ports/Enum.sol new file mode 100644 index 00000000..1aee93b2 --- /dev/null +++ b/contracts/solc-0.8/dep-ports/Enum.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity >=0.8.0; + +/// @title Enum - Collection of enums +/// @author Richard Meissner - +contract Enum { + enum Operation { + Call, + DelegateCall + } +} diff --git a/contracts/solc-0.8/dep-ports/IERC20.sol b/contracts/solc-0.8/dep-ports/IERC20.sol new file mode 100644 index 00000000..35f0cbf1 --- /dev/null +++ b/contracts/solc-0.8/dep-ports/IERC20.sol @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0; + +/** + * @dev Interface of the ERC20 standard as defined in the EIP. Does not include + * the optional functions; to access them see {ERC20Detailed}. + */ +interface IERC20 { + /** + * @dev Returns the amount of tokens in existence. + */ + function totalSupply() external view returns (uint256); + + /** + * @dev Returns the amount of tokens owned by `account`. + */ + function balanceOf(address account) external view returns (uint256); + + /** + * @dev Moves `amount` tokens from the caller's account to `recipient`. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transfer(address recipient, uint256 amount) external returns (bool); + + /** + * @dev Returns the remaining number of tokens that `spender` will be + * allowed to spend on behalf of `owner` through {transferFrom}. This is + * zero by default. + * + * This value changes when {approve} or {transferFrom} are called. + */ + function allowance(address owner, address spender) external view returns (uint256); + + /** + * @dev Sets `amount` as the allowance of `spender` over the caller's tokens. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * IMPORTANT: Beware that changing an allowance with this method brings the risk + * that someone may use both the old and the new allowance by unfortunate + * transaction ordering. One possible solution to mitigate this race + * condition is to first reduce the spender's allowance to 0 and set the + * desired value afterwards: + * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 + * + * Emits an {Approval} event. + */ + function approve(address spender, uint256 amount) external returns (bool); + + /** + * @dev Moves `amount` tokens from `sender` to `recipient` using the + * allowance mechanism. `amount` is then deducted from the caller's + * allowance. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transferFrom(address sender, address recipient, uint256 amount) external returns (bool); + + /** + * @dev Emitted when `value` tokens are moved from one account (`from`) to + * another (`to`). + * + * Note that `value` may be zero. + */ + event Transfer(address indexed from, address indexed to, uint256 value); + + /** + * @dev Emitted when the allowance of a `spender` for an `owner` is set by + * a call to {approve}. `value` is the new allowance. + */ + event Approval(address indexed owner, address indexed spender, uint256 value); +} diff --git a/contracts/solc-0.8/dep-ports/IGnosisSafeProxyFactory.sol b/contracts/solc-0.8/dep-ports/IGnosisSafeProxyFactory.sol new file mode 100644 index 00000000..97e0760d --- /dev/null +++ b/contracts/solc-0.8/dep-ports/IGnosisSafeProxyFactory.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity >=0.8.0; + +interface IGnosisSafeProxyFactory { + function createProxyWithNonce( + address impl, + bytes calldata initializer, + uint256 saltNonce + ) external returns (address payable); + + /// @dev Allows to retrieve the runtime code of a deployed Proxy. This can be used to check that the expected Proxy was deployed. + function proxyRuntimeCode() external pure returns (bytes memory); + + /// @dev Allows to retrieve the creation code used for the Proxy deployment. With this it is easily possible to calculate predicted address. + function proxyCreationCode() external pure returns (bytes memory); +} diff --git a/contracts/solc-0.8/dep-ports/ISignatureValidator.sol b/contracts/solc-0.8/dep-ports/ISignatureValidator.sol new file mode 100644 index 00000000..f967a06f --- /dev/null +++ b/contracts/solc-0.8/dep-ports/ISignatureValidator.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity >=0.8.0; + +// bytes4(keccak256("isValidSignature(bytes,bytes)") +bytes4 constant EIP1271_MAGIC_VALUE = 0x20c13b0b; + +interface ISignatureValidator { + + /** + * @dev Should return whether the signature provided is valid for the provided data + * @param _data Arbitrary length data signed on the behalf of address(this) + * @param _signature Signature byte array associated with _data + * + * MUST return the bytes4 magic value 0x20c13b0b when function passes. + * MUST NOT modify state (using STATICCALL for solc < 0.5, view modifier for solc > 0.5) + * MUST allow external calls + */ + function isValidSignature( + bytes memory _data, + bytes memory _signature) + external + view + returns (bytes4); +} diff --git a/migrations-ts/1-deploy-contracts.ts b/migrations-ts/1-deploy-contracts.ts index c9535415..1ffd67a5 100644 --- a/migrations-ts/1-deploy-contracts.ts +++ b/migrations-ts/1-deploy-contracts.ts @@ -1,12 +1,10 @@ -module.exports = function(deployer: Truffle.Deployer, network: string) { +module.exports = async function(deployer: Truffle.Deployer, network: string) { const deploy = ( name: string ): Truffle.Deployer => deployer.deploy(artifacts.require(name as any)); - ['Migrations', 'CPKFactory'].forEach(deploy); - if (network === 'test' || network === 'local') { - [ + await Promise.all([ 'GnosisSafe', 'GnosisSafe2', 'GnosisSafeProxyFactory', @@ -16,8 +14,23 @@ module.exports = function(deployer: Truffle.Deployer, network: string) { 'DailyLimitModule', 'ERC20Mintable', 'ConditionalTokens' - ].forEach(deploy); + ].map(deploy)); } + + await deployer.deploy(artifacts.require('CPKFactoryV1')); + + await deployer.deploy( + artifacts.require('CPKFactory'), + artifacts.require('GnosisSafeProxyFactory').address, + ); + + await deployer.deploy( + artifacts.require('CPKFactoryFacade'), + artifacts.require('CPKFactory').address, + artifacts.require('GnosisSafe').address, + web3.utils.keccak256(web3.utils.utf8ToHex('Contract Proxy Kit')), + artifacts.require('DefaultCallbackHandler').address, + ); } as Truffle.Migration; export { }; diff --git a/package.json b/package.json index 3998b935..d188a36e 100644 --- a/package.json +++ b/package.json @@ -6,8 +6,11 @@ "module": "lib/esm/index.js", "scripts": { "generate-types": "typechain --target=truffle-v5 './build/contracts/*.json'", - "migrate": "tsc -p ./tsconfig.migrate.json --outDir ./migrations && truffle migrate --network local", - "test-ts": "TS_NODE_PROJECT='./tsconfig.cjs.json' nyc mocha -t 20000 -r ts-node/register -r jsdom-global/register ./test/contract-proxy-kit.ts --exit", + "compile-0.5": "truffle compile --config ./truffle-config.solc-0.5.js", + "compile-0.8": "truffle compile --config ./truffle-config.solc-0.8.js", + "compile-contracts": "yarn compile-0.8 && yarn compile-0.5", + "migrate": "tsc -p ./tsconfig.migrate.json --outDir ./migrations && yarn compile-contracts && truffle migrate --network local --config ./truffle-config.solc-0.8.js", + "test-ts": "TS_NODE_PROJECT='./tsconfig.cjs.json' nyc mocha -t 40000 -r ts-node/register -r jsdom-global/register ./test/contract-proxy-kit.ts --exit", "test": "yarn generate-types && yarn migrate && yarn test-ts", "test-rpc": "run-with-testrpc --noVMErrorsOnRPCResponse 'yarn test'", "coverage": "nyc report --reporter=text-lcov | coveralls", diff --git a/src/abis/CpkFactoryAbi.json b/src/abis/CpkFactoryAbi.json index 71916404..06a909ab 100644 --- a/src/abis/CpkFactoryAbi.json +++ b/src/abis/CpkFactoryAbi.json @@ -1,59 +1,176 @@ [ { - "constant": true, - "inputs": [], - "name": "proxyCreationCode", - "outputs": [ + "inputs": [ { - "internalType": "bytes", - "name": "", - "type": "bytes" + "internalType": "contract IGnosisSafeProxyFactory", + "name": "_gnosisSafeProxyFactory", + "type": "address" } ], - "payable": false, - "stateMutability": "pure", - "type": "function" + "stateMutability": "nonpayable", + "type": "constructor" }, { - "constant": false, + "anonymous": false, "inputs": [ { + "indexed": true, + "internalType": "address", + "name": "proxy", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "initialImpl", + "type": "address" + }, + { + "indexed": false, "internalType": "address", - "name": "masterCopy", + "name": "initialOwner", "type": "address" }, { + "indexed": false, "internalType": "uint256", - "name": "saltNonce", + "name": "salt", "type": "uint256" - }, + } + ], + "name": "CPKCreation", + "type": "event" + }, + { + "inputs": [], + "name": "gnosisSafeProxyFactory", + "outputs": [ + { + "internalType": "contract IGnosisSafeProxyFactory", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function", + "constant": true + }, + { + "inputs": [], + "name": "proxyExtCodeHash", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function", + "constant": true + }, + { + "inputs": [], + "name": "proxyImplSetter", + "outputs": [ + { + "internalType": "contract ProxyImplSetter", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function", + "constant": true + }, + { + "inputs": [], + "name": "version", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function", + "constant": true + }, + { + "inputs": [], + "name": "proxyCreationCode", + "outputs": [ + { + "internalType": "bytes", + "name": "", + "type": "bytes" + } + ], + "stateMutability": "view", + "type": "function", + "constant": true + }, + { + "inputs": [], + "name": "proxyRuntimeCode", + "outputs": [ + { + "internalType": "bytes", + "name": "", + "type": "bytes" + } + ], + "stateMutability": "view", + "type": "function", + "constant": true + }, + { + "inputs": [ { "internalType": "address", - "name": "fallbackHandler", + "name": "owner", "type": "address" }, { "internalType": "address", - "name": "to", + "name": "safeVersion", "type": "address" }, { "internalType": "uint256", - "name": "value", + "name": "salt", "type": "uint256" }, { - "internalType": "bytes", - "name": "data", - "type": "bytes" + "components": [ + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + }, + { + "internalType": "enum TxReaction", + "name": "reaction", + "type": "uint8" + } + ], + "internalType": "struct CPKFactoryTx[]", + "name": "txs", + "type": "tuple[]" }, { - "internalType": "enum Enum.Operation", - "name": "operation", - "type": "uint8" + "internalType": "bytes", + "name": "signature", + "type": "bytes" } ], - "name": "createProxyAndExecTransaction", + "name": "createProxyAndExecTransactions", "outputs": [ { "internalType": "bool", @@ -61,8 +178,8 @@ "type": "bool" } ], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" + "stateMutability": "payable", + "type": "function", + "payable": true } ] diff --git a/src/config/networks.ts b/src/config/networks.ts index bf9bcd51..cbf93448 100644 --- a/src/config/networks.ts +++ b/src/config/networks.ts @@ -1,14 +1,14 @@ import { Address } from '../utils/basicTypes' -interface MasterCopyAddressVersion { - version: string - address: Address +interface ProxySearchParams { + proxyFactoryAddress: Address + initialImplAddress: Address } export interface NetworkConfigEntry { + proxySearchParams?: ProxySearchParams[] + proxyFactoryAddress?: Address masterCopyAddress?: Address - masterCopyAddressVersions?: Array - proxyFactoryAddress: Address multiSendAddress: Address fallbackHandlerAddress: Address } @@ -18,8 +18,8 @@ export interface NetworksConfig { } export interface NormalizedNetworkConfigEntry { - masterCopyAddressVersions: MasterCopyAddressVersion[] - proxyFactoryAddress: Address + proxySearchParams: ProxySearchParams[] + masterCopyAddress: Address multiSendAddress: Address fallbackHandlerAddress: Address } @@ -28,44 +28,72 @@ export interface NormalizedNetworksConfig { [id: string]: NormalizedNetworkConfigEntry } -// First element belongs to latest release. Do not alter this order. New releases go first. -const masterCopyAddressVersions: MasterCopyAddressVersion[] = [ - { - version: '1.2.0', - address: '0x6851D6fDFAfD08c0295C392436245E5bc78B0185' - }, - { - version: '1.1.1', - address: '0x34CfAC646f301356fAa8B21e94227e3583Fe3F5F' - } -] +const defaultMasterCopyAddress = '0x6851D6fDFAfD08c0295C392436245E5bc78B0185' export const defaultNetworks: NormalizedNetworksConfig = { // mainnet 1: { - masterCopyAddressVersions, - proxyFactoryAddress: '0x0fB4340432e56c014fa96286de17222822a9281b', + // For proxy search params, the first element belongs to latest release. + // Do not alter this order. New releases go first. + proxySearchParams: [ + { + proxyFactoryAddress: '0x0fB4340432e56c014fa96286de17222822a9281b', + initialImplAddress: defaultMasterCopyAddress + }, + { + proxyFactoryAddress: '0x0fB4340432e56c014fa96286de17222822a9281b', + initialImplAddress: '0x34CfAC646f301356fAa8B21e94227e3583Fe3F5F' + }, + ], + masterCopyAddress: defaultMasterCopyAddress, multiSendAddress: '0x8D29bE29923b68abfDD21e541b9374737B49cdAD', fallbackHandlerAddress: '0x40A930851BD2e590Bd5A5C981b436de25742E980' }, // rinkeby 4: { - masterCopyAddressVersions, - proxyFactoryAddress: '0x336c19296d3989e9e0c2561ef21c964068657c38', + proxySearchParams: [ + { + proxyFactoryAddress: '0x336c19296d3989e9e0c2561ef21c964068657c38', + initialImplAddress: defaultMasterCopyAddress + }, + { + proxyFactoryAddress: '0x336c19296d3989e9e0c2561ef21c964068657c38', + initialImplAddress: '0x34CfAC646f301356fAa8B21e94227e3583Fe3F5F' + }, + ], + masterCopyAddress: defaultMasterCopyAddress, multiSendAddress: '0x8D29bE29923b68abfDD21e541b9374737B49cdAD', fallbackHandlerAddress: '0x40A930851BD2e590Bd5A5C981b436de25742E980' }, // goerli 5: { - masterCopyAddressVersions, - proxyFactoryAddress: '0xfC7577774887aAE7bAcdf0Fc8ce041DA0b3200f7', + proxySearchParams: [ + { + proxyFactoryAddress: '0xfC7577774887aAE7bAcdf0Fc8ce041DA0b3200f7', + initialImplAddress: defaultMasterCopyAddress + }, + { + proxyFactoryAddress: '0xfC7577774887aAE7bAcdf0Fc8ce041DA0b3200f7', + initialImplAddress: '0x34CfAC646f301356fAa8B21e94227e3583Fe3F5F' + }, + ], + masterCopyAddress: defaultMasterCopyAddress, multiSendAddress: '0x8D29bE29923b68abfDD21e541b9374737B49cdAD', fallbackHandlerAddress: '0x40A930851BD2e590Bd5A5C981b436de25742E980' }, // kovan 42: { - masterCopyAddressVersions, - proxyFactoryAddress: '0xfC7577774887aAE7bAcdf0Fc8ce041DA0b3200f7', + proxySearchParams: [ + { + proxyFactoryAddress: '0xfC7577774887aAE7bAcdf0Fc8ce041DA0b3200f7', + initialImplAddress: defaultMasterCopyAddress + }, + { + proxyFactoryAddress: '0xfC7577774887aAE7bAcdf0Fc8ce041DA0b3200f7', + initialImplAddress: '0x34CfAC646f301356fAa8B21e94227e3583Fe3F5F' + }, + ], + masterCopyAddress: defaultMasterCopyAddress, multiSendAddress: '0x8D29bE29923b68abfDD21e541b9374737B49cdAD', fallbackHandlerAddress: '0x40A930851BD2e590Bd5A5C981b436de25742E980' } @@ -81,25 +109,25 @@ export function normalizeNetworksConfig( const normalizedNetworks: NormalizedNetworksConfig = {} for (const networkId of Object.keys(networks)) { const currentNetwork = networks[networkId] - let mcVersions: MasterCopyAddressVersion[] = [] - if (currentNetwork.masterCopyAddress) { - mcVersions = [ + let searchParams: ProxySearchParams[] = [] + if (currentNetwork.proxySearchParams) { + searchParams = currentNetwork.proxySearchParams + } else if (currentNetwork.proxyFactoryAddress && currentNetwork.masterCopyAddress) { + searchParams = [ { - version: masterCopyAddressVersions[0].version, - address: currentNetwork.masterCopyAddress + proxyFactoryAddress: currentNetwork.proxyFactoryAddress, + initialImplAddress: currentNetwork.masterCopyAddress, } ] - } else if (currentNetwork.masterCopyAddressVersions) { - mcVersions = currentNetwork.masterCopyAddressVersions } - if (mcVersions.length === 0) { + if (searchParams.length === 0) { throw new Error( - 'Properties "masterCopyAddress" or "masterCopyAddressVersions" are missing in CPK network configuration' + 'Properties "proxySearchParams" or "proxyFactoryAddress" and "masterCopyAddress" are missing in CPK network configuration' ) } normalizedNetworks[networkId] = { - masterCopyAddressVersions: mcVersions, - proxyFactoryAddress: currentNetwork.proxyFactoryAddress, + proxySearchParams: searchParams, + masterCopyAddress: currentNetwork.masterCopyAddress || defaultMasterCopyAddress, multiSendAddress: currentNetwork.multiSendAddress, fallbackHandlerAddress: currentNetwork.fallbackHandlerAddress } diff --git a/src/contractManager/index.ts b/src/contractManager/index.ts index 30b0f2ea..d096aec8 100644 --- a/src/contractManager/index.ts +++ b/src/contractManager/index.ts @@ -21,7 +21,7 @@ export interface ContractManagerProps { class ContractManager { #versionUtils?: ContractVersionUtils #contract?: Contract - #proxyFactory: Contract + #proxyFactory?: Contract #masterCopyAddress: Address #multiSend: Contract #fallbackHandlerAddress: Address @@ -33,10 +33,9 @@ class ContractManager { } constructor(ethLibAdapter: EthLibAdapter, network: NormalizedNetworkConfigEntry) { - this.#masterCopyAddress = network.masterCopyAddressVersions[0].address + this.#masterCopyAddress = network.masterCopyAddress this.#fallbackHandlerAddress = network.fallbackHandlerAddress this.#multiSend = ethLibAdapter.getContract(multiSendAbi, network.multiSendAddress) - this.#proxyFactory = ethLibAdapter.getContract(cpkFactoryAbi, network.proxyFactoryAddress) } async init(opts: ContractManagerProps): Promise { @@ -47,6 +46,7 @@ class ContractManager { const { ethLibAdapter, ownerAccount, saltNonce, network, isSafeApp, isConnectedToSafe } = opts let proxyAddress let properVersion + let proxyFactory if (isSafeApp || isConnectedToSafe) { const temporaryContract = ethLibAdapter.getContract(safeAbiV111, ownerAccount) @@ -57,9 +57,11 @@ class ContractManager { ethLibAdapter.abiEncode(['address', 'uint256'], [ownerAccount, saltNonce]) ) - for (const masterCopyVersion of network.masterCopyAddressVersions) { + for (const { proxyFactoryAddress, initialImplAddress } of network.proxySearchParams) { + proxyFactory = ethLibAdapter.getContract(cpkFactoryAbi, proxyFactoryAddress) proxyAddress = await this.calculateProxyAddress( - masterCopyVersion.address, + proxyFactory, + initialImplAddress, salt, ethLibAdapter ) @@ -75,9 +77,12 @@ class ContractManager { if (!properVersion) { // Last version released - properVersion = network.masterCopyAddressVersions[0].version + const { proxyFactoryAddress, initialImplAddress } = network.proxySearchParams[0] + properVersion = '1.2.0' + proxyFactory = ethLibAdapter.getContract(cpkFactoryAbi, proxyFactoryAddress) proxyAddress = await this.calculateProxyAddress( - network.masterCopyAddressVersions[0].address, + proxyFactory, + initialImplAddress, salt, ethLibAdapter ) @@ -96,22 +101,49 @@ class ContractManager { default: throw new Error('CPK Proxy version is not valid') } + + this.#proxyFactory = proxyFactory } private async calculateProxyAddress( - masterCopyAddress: Address, + proxyFactory: Contract, + initialImplAddress: Address, salt: string, ethLibAdapter: EthLibAdapter ): Promise
{ + let proxyFactoryVersion; + + try { + proxyFactoryVersion = (await proxyFactory.call('version', [])).toString() + } catch(e) {} + const initCode = ethLibAdapter.abiEncodePacked( - { type: 'bytes', value: await this.#proxyFactory.call('proxyCreationCode', []) }, + { type: 'bytes', value: await proxyFactory.call('proxyCreationCode', []) }, { type: 'bytes', - value: ethLibAdapter.abiEncode(['address'], [masterCopyAddress]) + value: ethLibAdapter.abiEncode(['address'], [initialImplAddress]) } ) + + let proxyDeployer; + if (proxyFactoryVersion == '2') { + salt = ethLibAdapter.keccak256( + ethLibAdapter.abiEncodePacked( + { + type: "bytes32", + // hardcoded keccak256('') because Web3 returns null for that in v1 + value: '0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470' + }, + { type: "bytes32", value: salt }, + ) + ); + proxyDeployer = await proxyFactory.call('gnosisSafeProxyFactory', []); + } else { + proxyDeployer = proxyFactory.address + } + const proxyAddress = ethLibAdapter.calcCreate2Address( - this.#proxyFactory.address, + proxyDeployer, salt, initCode ) @@ -126,7 +158,7 @@ class ContractManager { return this.#contract } - get proxyFactory(): Contract { + get proxyFactory(): Contract | undefined { return this.#proxyFactory } diff --git a/src/transactionManagers/CpkTransactionManager/index.ts b/src/transactionManagers/CpkTransactionManager/index.ts index b9302f01..43a868b5 100644 --- a/src/transactionManagers/CpkTransactionManager/index.ts +++ b/src/transactionManagers/CpkTransactionManager/index.ts @@ -1,6 +1,6 @@ import EthLibAdapter, { Contract } from '../../ethLibAdapters/EthLibAdapter' import { Address, NumberLike } from '../../utils/basicTypes' -import { zeroAddress } from '../../utils/constants' +import { defaultTxData, sentinelOwner, zeroAddress } from '../../utils/constants' import { NormalizeGas, OperationType, @@ -9,7 +9,8 @@ import { StandardTransaction, Transaction, TransactionError, - TransactionResult + TransactionResult, + TxReaction } from '../../utils/transactions' import TransactionManager, { ExecTransactionProps, @@ -56,23 +57,29 @@ class CpkTransactionManager implements TransactionManager { return this.execTxsWhileConnectedToSafe(ethLibAdapter, transactions, sendOptions) } - // (r, s, v) where v is 1 means this signature is approved by the address encoded in value r - // "Hashes are automatically approved by the sender of the message" - const autoApprovedSignature = ethLibAdapter.abiEncodePacked( - { type: 'uint256', value: ownerAccount }, // r - { type: 'uint256', value: 0 }, // s - { type: 'uint8', value: 1 } // v - ) - - const txObj: ContractTxObj = isDeployed - ? this.getSafeProxyTxObj(safeContract, safeExecTxParams, autoApprovedSignature) - : this.getCPKFactoryTxObj( - masterCopyAddress, - fallbackHandlerAddress, - safeExecTxParams, - saltNonce, - proxyFactory - ) + let txObj + if (isDeployed) { + txObj = this.getSafeProxyTxObj( + safeContract, + safeExecTxParams, + this.makeAutoApprovedSignature(ethLibAdapter, ownerAccount), + ); + } else { + if (!proxyFactory) { + throw new Error('missing proxy factory for undeployed transactions'); + } + txObj = this.getCPKFactoryTxObj( + ownerAccount, + masterCopyAddress, + saltNonce, + fallbackHandlerAddress, + safeExecTxParams, + this.makeAutoApprovedSignature(ethLibAdapter, ownerAccount), + this.makeAutoApprovedSignature(ethLibAdapter, proxyFactory.address), + proxyFactory, + safeContract, + ); + } const { success, gasLimit } = await this.findGasLimit(ethLibAdapter, txObj, sendOptions) sendOptions.gas = gasLimit @@ -145,16 +152,70 @@ class CpkTransactionManager implements TransactionManager { } private getCPKFactoryTxObj( + ownerAccount: Address, masterCopyAddress: Address, + salt: string, fallbackHandlerAddress: Address, - { to, value, data, operation }: StandardTransaction, - saltNonce: string, - proxyFactory: Contract + transaction: StandardTransaction, + safeOwnerApprovedSignature: string, + safeFactoryApprovedSignature: string, + proxyFactory: Contract, + safeContract: Contract, ): ContractTxObj { + const execTxObj = this.getSafeProxyTxObj(safeContract, transaction, safeFactoryApprovedSignature); + const swapTxObj = this.getSafeProxyTxObj( + safeContract, + { + to: safeContract.address, + value: 0, + data: safeContract.encode('swapOwner', [ + sentinelOwner, + proxyFactory.address, + ownerAccount, + ]), + operation: OperationType.Call, + }, + safeFactoryApprovedSignature, + ); + const txs = [ + { + // setup new Safe with the factory as the owner + value: 0, + data: safeContract.encode('setup', [ + [proxyFactory.address], // address[] owners + 1, // uint256 threshold + zeroAddress, // address to + defaultTxData, // bytes data + fallbackHandlerAddress, // address fallbackHandler + zeroAddress, // address paymentToken + 0, // uint256 payment + zeroAddress, // address payable paymentReceiver + ]), + reaction: TxReaction.IgnoreReturn, + }, + { + // execute first transactions with the factory + value: transaction.value, + data: execTxObj.contract.encode(execTxObj.methodName, execTxObj.params), + reaction: TxReaction.CaptureBoolReturn, + }, + { + // change the owner of the Safe from the factory to the owner account + value: 0, + data: swapTxObj.contract.encode(swapTxObj.methodName, swapTxObj.params), + reaction: TxReaction.RevertOnReturnFalse, + }, + ] return { contract: proxyFactory, - methodName: 'createProxyAndExecTransaction', - params: [masterCopyAddress, saltNonce, fallbackHandlerAddress, to, value, data, operation] + methodName: 'createProxyAndExecTransactions', + params: [ + ownerAccount, + masterCopyAddress, + salt, + txs, + safeOwnerApprovedSignature, + ] } } @@ -248,6 +309,19 @@ class CpkTransactionManager implements TransactionManager { } return new TransactionError(errorMessage, revertData, revertMessage) } + + private makeAutoApprovedSignature( + ethLibAdapter: EthLibAdapter, + msgSender: Address + ): string { + // (r, s, v) where v is 1 means this signature is approved by the address encoded in value r + // "Hashes are automatically approved by the sender of the message" + return ethLibAdapter.abiEncodePacked( + { type: 'uint256', value: msgSender }, // r + { type: 'uint256', value: 0 }, // s + { type: 'uint8', value: 1 } // v + ) + } } export default CpkTransactionManager diff --git a/src/utils/basicTypes.ts b/src/utils/basicTypes.ts index a2c23780..b8dcf859 100644 --- a/src/utils/basicTypes.ts +++ b/src/utils/basicTypes.ts @@ -1,4 +1,4 @@ -type Json = string | number | boolean | null | JsonObject | Json[] +type Json = string | number | boolean | null | undefined | JsonObject | Json[] type JsonObject = { [property: string]: Json } diff --git a/src/utils/constants.ts b/src/utils/constants.ts index c4f9260c..7b5e3a2c 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -3,6 +3,7 @@ import { OperationType } from './transactions' export const zeroAddress = `0x${'0'.repeat(40)}` export const sentinelModules = '0x0000000000000000000000000000000000000001' +export const sentinelOwner = '0x0000000000000000000000000000000000000001' export const defaultTxOperation = OperationType.Call export const defaultTxValue = 0 diff --git a/src/utils/transactions.ts b/src/utils/transactions.ts index 9f150972..b89a3208 100644 --- a/src/utils/transactions.ts +++ b/src/utils/transactions.ts @@ -7,6 +7,12 @@ export enum OperationType { DelegateCall // 1 } +export enum TxReaction { + RevertOnReturnFalse, + CaptureBoolReturn, + IgnoreReturn +} + interface GasLimitOptions { gas?: NumberLike gasLimit?: NumberLike diff --git a/test/ethers/shouldWorkWithEthers.ts b/test/ethers/shouldWorkWithEthers.ts index 41f94ece..c5408eb5 100644 --- a/test/ethers/shouldWorkWithEthers.ts +++ b/test/ethers/shouldWorkWithEthers.ts @@ -126,21 +126,25 @@ export function shouldWorkWithEthers({ let networks: NetworksConfig before('obtain addresses from artifacts', async () => { - const { gnosisSafe, gnosisSafe2, cpkFactory, multiSend, defaultCallbackHandler } = contracts + const { gnosisSafe, gnosisSafe2, cpkFactory, cpkFactoryV1, multiSend, defaultCallbackHandler } = contracts networks = { [(await signer.provider.getNetwork()).chainId]: { - masterCopyAddressVersions: [ + proxySearchParams: [ { - address: gnosisSafe.address, - version: '1.2.0' + proxyFactoryAddress: cpkFactory.address, + initialImplAddress: await cpkFactory.proxyImplSetter(), }, { - address: gnosisSafe2.address, - version: '1.1.1' - } + proxyFactoryAddress: cpkFactoryV1.address, + initialImplAddress: gnosisSafe.address, + }, + { + proxyFactoryAddress: cpkFactoryV1.address, + initialImplAddress: gnosisSafe2.address, + }, ], - proxyFactoryAddress: cpkFactory.address, + masterCopyAddress: gnosisSafe.address, multiSendAddress: multiSend.address, fallbackHandlerAddress: defaultCallbackHandler.address } diff --git a/test/utils/contracts.ts b/test/utils/contracts.ts index 9ef79cc7..ec6bdd19 100644 --- a/test/utils/contracts.ts +++ b/test/utils/contracts.ts @@ -2,6 +2,7 @@ const TruffleContract = require('@truffle/contract') import Web3Maj1Min3 from 'web3-1-3' import ConditionalTokensJson from '../../build/contracts/ConditionalTokens.json' import CPKFactoryJson from '../../build/contracts/CPKFactory.json' +import CPKFactoryV1Json from '../../build/contracts/CPKFactoryV1.json' import DailyLimitModuleJson from '../../build/contracts/DailyLimitModule.json' import DefaultCallbackHandlerJson from '../../build/contracts/DefaultCallbackHandler.json' import ERC20MintableJson from '../../build/contracts/ERC20Mintable.json' @@ -13,6 +14,7 @@ import MultistepJson from '../../build/contracts/Multistep.json' import { Address } from '../../src/utils/basicTypes' let CPKFactory: any +let CPKFactoryV1: any let GnosisSafe: any let GnosisSafe2: any let GnosisSafeProxyFactory: any @@ -24,6 +26,7 @@ let ConditionalTokens: any let DailyLimitModule: any let cpkFactory: any +let cpkFactoryV1: any let gnosisSafe: any let gnosisSafe2: any let gnosisSafeProxyFactory: any @@ -36,6 +39,7 @@ let dailyLimitModule: any export interface TestContractInstances { cpkFactory: any + cpkFactoryV1: any gnosisSafe: any gnosisSafe2: any gnosisSafeProxyFactory: any @@ -49,6 +53,7 @@ export interface TestContractInstances { export interface TestContracts { CPKFactory: any + CPKFactoryV1: any GnosisSafe: any GnosisSafe2: any GnosisSafeProxyFactory: any @@ -68,6 +73,11 @@ export const initializeContracts = async (safeOwner: Address): Promise => CPKFactory.defaults({ from: safeOwner }) cpkFactory = await CPKFactory.deployed() + CPKFactoryV1 = TruffleContract(CPKFactoryV1Json) + CPKFactoryV1.setProvider(provider) + CPKFactoryV1.defaults({ from: safeOwner }) + cpkFactoryV1 = await CPKFactoryV1.deployed() + GnosisSafe = TruffleContract(GnosisSafeJson) GnosisSafe.setProvider(provider) GnosisSafe.defaults({ from: safeOwner }) @@ -116,6 +126,7 @@ export const initializeContracts = async (safeOwner: Address): Promise => export const getContracts = (): TestContracts => ({ CPKFactory, + CPKFactoryV1, GnosisSafe, GnosisSafe2, GnosisSafeProxyFactory, @@ -129,6 +140,7 @@ export const getContracts = (): TestContracts => ({ export const getContractInstances = (): TestContractInstances => ({ cpkFactory, + cpkFactoryV1, gnosisSafe, gnosisSafe2, gnosisSafeProxyFactory, diff --git a/test/web3/shouldWorkWithWeb3.ts b/test/web3/shouldWorkWithWeb3.ts index a4bb8141..5819829b 100644 --- a/test/web3/shouldWorkWithWeb3.ts +++ b/test/web3/shouldWorkWithWeb3.ts @@ -114,21 +114,25 @@ export function shouldWorkWithWeb3({ let networks: NetworksConfig before('obtain addresses from artifacts', async () => { - const { gnosisSafe, gnosisSafe2, cpkFactory, multiSend, defaultCallbackHandler } = contracts + const { gnosisSafe, gnosisSafe2, cpkFactory, cpkFactoryV1, multiSend, defaultCallbackHandler } = contracts networks = { [await ueb3.eth.net.getId()]: { - masterCopyAddressVersions: [ + proxySearchParams: [ { - address: gnosisSafe.address, - version: '1.2.0' + proxyFactoryAddress: cpkFactory.address, + initialImplAddress: await cpkFactory.proxyImplSetter(), }, { - address: gnosisSafe2.address, - version: '1.1.1' - } + proxyFactoryAddress: cpkFactoryV1.address, + initialImplAddress: gnosisSafe.address, + }, + { + proxyFactoryAddress: cpkFactoryV1.address, + initialImplAddress: gnosisSafe2.address, + }, ], - proxyFactoryAddress: cpkFactory.address, + masterCopyAddress: gnosisSafe.address, multiSendAddress: multiSend.address, fallbackHandlerAddress: defaultCallbackHandler.address } diff --git a/truffle-config.js b/truffle-config.base.js similarity index 100% rename from truffle-config.js rename to truffle-config.base.js diff --git a/truffle-config.solc-0.5.js b/truffle-config.solc-0.5.js new file mode 100644 index 00000000..6b5f9e06 --- /dev/null +++ b/truffle-config.solc-0.5.js @@ -0,0 +1,16 @@ +const baseSettings = require('./truffle-config.base'); + +module.exports = { + ...baseSettings, + contracts_directory: "./contracts/solc-0.5", + compilers: { + solc: { + version: "0.5.17", + settings: { + optimizer: { + enabled: true, + } + } + }, + }, +}; diff --git a/truffle-config.solc-0.8.js b/truffle-config.solc-0.8.js new file mode 100644 index 00000000..751a478c --- /dev/null +++ b/truffle-config.solc-0.8.js @@ -0,0 +1,16 @@ +const baseSettings = require('./truffle-config.base'); + +module.exports = { + ...baseSettings, + contracts_directory: "./contracts/solc-0.8", + compilers: { + solc: { + version: "0.8.0", + settings: { + optimizer: { + enabled: true, + } + } + }, + }, +};