• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

safe-global / safe-eth-py / 9987214757

18 Jul 2024 07:18AM UTC coverage: 93.565% (+0.01%) from 93.555%
9987214757

push

github

web-flow
Add addresses 1.3.0 for chain ETHERLINK_MAINNET (#1226)

* Add new chain 42793

* Add new explorer client URL: https://explorer.etherlink.com/api/v1/graphql

* Add new master copy address 0x69f4D1788e39c87893C980c06EdF4b7f686e2938

* Add new master copy address 0xfb1bffC9d739B8D520DaF37dF666da4C687191EA

* Add new proxy address 0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

1 of 1 new or added line in 1 file covered. (100.0%)

11 existing lines in 6 files now uncovered.

8142 of 8702 relevant lines covered (93.56%)

3.74 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.94
/gnosis/safe/safe_create2_tx.py
1
import math
4✔
2
from logging import getLogger
4✔
3
from typing import List, NamedTuple, Optional, Sequence
4✔
4

5
from eth_abi.packed import encode_packed
4✔
6
from eth_typing import ChecksumAddress
4✔
7
from hexbytes import HexBytes
4✔
8
from web3 import Web3
4✔
9
from web3.types import TxParams, Wei
4✔
10

11
from gnosis.eth.constants import GAS_CALL_DATA_BYTE, NULL_ADDRESS
4✔
12
from gnosis.eth.contracts import (
4✔
13
    get_proxy_factory_contract,
14
    get_safe_contract,
15
    get_safe_V1_0_0_contract,
16
    get_safe_V1_1_1_contract,
17
    get_safe_V1_3_0_contract,
18
    get_safe_V1_4_1_contract,
19
)
20
from gnosis.eth.utils import (
4✔
21
    fast_is_checksum_address,
22
    get_empty_tx_params,
23
    mk_contract_address_2,
24
)
25

26
from .constants import TOKEN_TRANSFER_GAS
4✔
27

28
logger = getLogger(__name__)
4✔
29

30

31
class SafeCreate2Tx(NamedTuple):
4✔
32
    salt_nonce: int
4✔
33
    owners: List[str]
4✔
34
    threshold: int
4✔
35
    fallback_handler: str
4✔
36
    master_copy_address: str
4✔
37
    proxy_factory_address: str
4✔
38
    payment_receiver: str
4✔
39
    payment_token: str
4✔
40
    payment: int
4✔
41
    gas: int
4✔
42
    gas_price: int
4✔
43
    payment_token_eth_value: float
4✔
44
    fixed_creation_cost: Optional[int]
4✔
45
    safe_address: str
4✔
46
    safe_setup_data: bytes
4✔
47

48
    @property
4✔
49
    def payment_ether(self):
4✔
50
        return self.gas * self.gas_price
4✔
51

52

53
class SafeCreate2TxBuilder:
4✔
54
    """
55
    Helper to create Safes using Safe's Proxy Factory with CREATE2
56
    """
57

58
    MASTER_COPY_VERSION_WITH_CONTRACT = {
4✔
59
        "1.4.1": get_safe_V1_4_1_contract,
60
        "1.3.0": get_safe_V1_3_0_contract,
61
        "1.1.1": get_safe_V1_1_1_contract,
62
        "1.0.0": get_safe_V1_0_0_contract,
63
    }
64

65
    def __init__(
4✔
66
        self,
67
        w3: Web3,
68
        master_copy_address: ChecksumAddress,
69
        proxy_factory_address: ChecksumAddress,
70
    ):
71
        """
72
        :param w3: Web3 instance
73
        :param master_copy_address: `Gnosis Safe` master copy address
74
        :param proxy_factory_address: `Gnosis Proxy Factory` address
75
        """
76
        assert fast_is_checksum_address(master_copy_address)
4✔
77
        assert fast_is_checksum_address(proxy_factory_address)
4✔
78

79
        self.w3 = w3
4✔
80
        self.master_copy_address = master_copy_address
4✔
81
        self.proxy_factory_address = proxy_factory_address
4✔
82

83
        # Check Safe master copy version
84
        self.safe_version = (
4✔
85
            get_safe_contract(w3, master_copy_address).functions.VERSION().call()
86
        )
87
        master_copy_contract_fn = self.MASTER_COPY_VERSION_WITH_CONTRACT.get(
4✔
88
            self.safe_version
89
        )
90
        if not master_copy_contract_fn:
4✔
91
            raise ValueError("Safe version must be 1.4.1, 1.3.0, 1.1.1 or 1.0.0")
×
92

93
        self.master_copy_contract = master_copy_contract_fn(w3, master_copy_address)
4✔
94

95
        self.proxy_factory_contract = get_proxy_factory_contract(
4✔
96
            w3, proxy_factory_address
97
        )
98

99
    def build(
4✔
100
        self,
101
        owners: Sequence[ChecksumAddress],
102
        threshold: int,
103
        salt_nonce: int,
104
        gas_price: int,
105
        fallback_handler: Optional[ChecksumAddress] = None,
106
        payment_receiver: Optional[ChecksumAddress] = None,
107
        payment_token: Optional[ChecksumAddress] = None,
108
        payment_token_eth_value: float = 1.0,
109
        fixed_creation_cost: Optional[int] = None,
110
    ) -> SafeCreate2Tx:
111
        """
112
        :param owners: Owners of the Safe
113
        :param threshold: Minimum number of users required to operate the Safe
114
        :param fallback_handler: Handler for fallback calls to the Safe
115
        :param salt_nonce: Web3 instance
116
        :param gas_price: Gas Price
117
        :param payment_receiver: Address to refund when the Safe is created. Address(0) if no need to refund
118
        :param payment_token: Payment token instead of paying the funder with ether. If None Ether will be used
119
        :param payment_token_eth_value: Value of payment token per 1 Ether
120
        :param fixed_creation_cost: Fixed creation cost of Safe (Wei)
121
        :return: SafeCreate2Tx with all the data for execution
122
        """
123

124
        assert 0 < threshold <= len(owners)
4✔
125
        fallback_handler = fallback_handler or NULL_ADDRESS
4✔
126
        payment_receiver = payment_receiver or NULL_ADDRESS
4✔
127
        payment_token = payment_token or NULL_ADDRESS
4✔
128
        assert fast_is_checksum_address(payment_receiver)
4✔
129
        assert fast_is_checksum_address(payment_token)
4✔
130

131
        # Get bytes for `setup(address[] calldata _owners, uint256 _threshold, address to, bytes calldata data,
132
        # address paymentToken, uint256 payment, address payable paymentReceiver)`
133
        # This initializer will be passed to the ProxyFactory to be called right after proxy is deployed
134
        # We use `payment=0` as safe has no ether yet and estimation will fail
135
        safe_setup_data: bytes = self._get_initial_setup_safe_data(
4✔
136
            owners,
137
            threshold,
138
            fallback_handler=fallback_handler,
139
            payment_token=payment_token,
140
            payment_receiver=payment_receiver,
141
        )
142

143
        calculated_gas: int = self._calculate_gas(
4✔
144
            owners, safe_setup_data, payment_token
145
        )
146
        estimated_gas: int = self._estimate_gas(
4✔
147
            safe_setup_data, salt_nonce, payment_token, payment_receiver
148
        )
149
        logger.debug("Magic gas %d - Estimated gas %d", calculated_gas, estimated_gas)
4✔
150
        gas = max(calculated_gas, estimated_gas)
4✔
151

152
        # Payment will be safe deploy cost
153
        payment = self._calculate_refund_payment(
4✔
154
            gas, gas_price, fixed_creation_cost, payment_token_eth_value
155
        )
156

157
        # Now we have a estimate for `payment` so we get initialization data again
158
        final_safe_setup_data: bytes = self._get_initial_setup_safe_data(
4✔
159
            owners,
160
            threshold,
161
            fallback_handler=fallback_handler,
162
            payment_token=payment_token,
163
            payment=payment,
164
            payment_receiver=payment_receiver,
165
        )
166

167
        safe_address = self.calculate_create2_address(final_safe_setup_data, salt_nonce)
4✔
168
        assert int(
4✔
169
            safe_address, 16
170
        ), "Calculated Safe address cannot be the NULL ADDRESS"
171

172
        return SafeCreate2Tx(
4✔
173
            salt_nonce,
174
            owners,
175
            threshold,
176
            fallback_handler,
177
            self.master_copy_address,
178
            self.proxy_factory_address,
179
            payment_receiver,
180
            payment_token,
181
            payment,
182
            gas,
183
            gas_price,
184
            payment_token_eth_value,
185
            fixed_creation_cost,
186
            safe_address,
187
            final_safe_setup_data,
188
        )
189

190
    @staticmethod
4✔
191
    def _calculate_gas(
4✔
192
        owners: List[str], safe_setup_data: bytes, payment_token: str
193
    ) -> int:
194
        """
195
        Calculate gas manually, based on tests of previously deployed Safes
196

197
        :param owners: Safe owners
198
        :param safe_setup_data: Data for proxy setup
199
        :param payment_token: If payment token, we will need more gas to transfer and maybe storage if first time
200
        :return: total gas needed for deployment
201
        """
202
        base_gas = 250_000  # Transaction base gas
4✔
203

204
        # If we already have the token, we don't have to pay for storage, so it will be just 5K instead of 60K.
205
        if payment_token != NULL_ADDRESS:
4✔
206
            payment_token_gas = TOKEN_TRANSFER_GAS
4✔
207
        else:
208
            payment_token_gas = 0
4✔
209

210
        data_gas = GAS_CALL_DATA_BYTE * len(safe_setup_data)  # Data gas
4✔
211
        gas_per_owner = (
4✔
212
            25_000  # Magic number calculated by testing and averaging owners
213
        )
214
        return base_gas + data_gas + payment_token_gas + len(owners) * gas_per_owner
4✔
215

216
    @staticmethod
4✔
217
    def _calculate_refund_payment(
4✔
218
        gas: int,
219
        gas_price: int,
220
        fixed_creation_cost: Optional[int],
221
        payment_token_eth_value: float,
222
    ) -> int:
223
        if fixed_creation_cost is None:
4✔
224
            # Payment will be safe deploy cost + transfer fees for sending ether to the deployer
225
            base_payment: int = gas * gas_price
4✔
226
            # Calculate payment for tokens using the conversion (if used)
227
            return math.ceil(base_payment / payment_token_eth_value)
4✔
228
        else:
229
            return fixed_creation_cost
4✔
230

231
    def calculate_create2_address(self, safe_setup_data: bytes, salt_nonce: int):
4✔
232
        proxy_creation_code = (
4✔
233
            self.proxy_factory_contract.functions.proxyCreationCode().call()
234
        )
235
        salt = self.w3.keccak(
4✔
236
            encode_packed(
237
                ["bytes", "uint256"], [self.w3.keccak(safe_setup_data), salt_nonce]
238
            )
239
        )
240
        deployment_data = encode_packed(
4✔
241
            ["bytes", "uint256"],
242
            [proxy_creation_code, int(self.master_copy_address, 16)],
243
        )
244
        return mk_contract_address_2(
4✔
245
            self.proxy_factory_contract.address, salt, deployment_data
246
        )
247

248
    def _estimate_gas(
4✔
249
        self,
250
        initializer: bytes,
251
        salt_nonce: int,
252
        payment_token: str,
253
        payment_receiver: str,
254
    ) -> int:
255
        """
256
        Estimate gas via `eth_estimateGas`.
257
        Payment cannot be estimated, as ether/tokens don't have to be in the calculated Safe address,
258
        so we add some gas later.
259

260
        :param initializer: Data initializer to send to GnosisSafe setup method
261
        :param salt_nonce: Nonce that will be used to generate the salt to calculate
262
        the address of the new proxy contract.
263
        :return: Total gas estimation
264
        """
265

266
        # Estimate the contract deployment. We cannot estimate the refunding, as the safe address has not any funds
267
        gas: int = self.proxy_factory_contract.functions.createProxyWithNonce(
4✔
268
            self.master_copy_address, initializer, salt_nonce
269
        ).estimate_gas()
270

271
        # We estimate the refund as a new tx
272
        if payment_token == NULL_ADDRESS:
4✔
273
            # Same cost to send 1 ether than 1000
274
            gas += self.w3.eth.estimate_gas({"to": payment_receiver, "value": Wei(1)})
4✔
275
        else:
276
            # Assume the worst scenario with a regular token transfer without storage
277
            # initialized (payment_receiver no previous owner of token)
278
            gas += 60_000
4✔
279

280
        # Add a little more for overhead
281
        gas += 20_000
4✔
282

283
        return gas
4✔
284

285
    def _get_initial_setup_safe_data(
4✔
286
        self,
287
        owners: List[str],
288
        threshold: int,
289
        fallback_handler: str = NULL_ADDRESS,
290
        payment_token: str = NULL_ADDRESS,
291
        payment: int = 0,
292
        payment_receiver: str = NULL_ADDRESS,
293
    ) -> bytes:
294
        empty_params: TxParams = get_empty_tx_params()
4✔
295

296
        if self.safe_version in ("1.4.1", "1.3.0", "1.1.1"):
4✔
297
            return HexBytes(
4✔
298
                self.master_copy_contract.functions.setup(
299
                    owners,
300
                    threshold,
301
                    NULL_ADDRESS,  # Contract address for optional delegate call
302
                    b"",  # Data payload for optional delegate call
303
                    fallback_handler,  # Handler for fallback calls to this contract
304
                    payment_token,
305
                    payment,
306
                    payment_receiver,
307
                ).build_transaction(empty_params)["data"]
308
            )
309
        elif self.safe_version == "1.0.0":
4✔
310
            return HexBytes(
4✔
311
                self.master_copy_contract.functions.setup(
312
                    owners,
313
                    threshold,
314
                    NULL_ADDRESS,  # Contract address for optional delegate call
315
                    b"",  # Data payload for optional delegate call
316
                    payment_token,
317
                    payment,
318
                    payment_receiver,
319
                ).build_transaction(empty_params)["data"]
320
            )
321
        else:
UNCOV
322
            raise ValueError("Safe version must be 1.4.1, 1.3.0, 1.1.1 or 1.0.0")
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc