22pragma solidity ^ 0.8.13 ;
33
44import {Vm} from "forge-std/Vm.sol " ;
5- import {HTTP} from "../lib/ solidity-http/src /HTTP.sol " ;
6- import {MultiSendCallOnly} from "../lib/ safe-smart-account/contracts /libraries/MultiSendCallOnly.sol " ;
7- import {Enum} from "../lib/ safe-smart-account/contracts /common/Enum.sol " ;
5+ import {HTTP} from "solidity-http/HTTP.sol " ;
6+ import {MultiSendCallOnly} from "safe-smart-account/libraries/MultiSendCallOnly.sol " ;
7+ import {Enum} from "safe-smart-account/common/Enum.sol " ;
88import {ISafeSmartAccount} from "./ISafeSmartAccount.sol " ;
99
1010library Safe {
1111 using HTTP for * ;
1212
13+ /// forge-lint: disable-next-line(screaming-snake-case-const)
1314 Vm constant vm = Vm (address (bytes20 (uint160 (uint256 (keccak256 ("hevm cheat code " ))))));
1415
1516 // https://github.com/safe-global/safe-smart-account/blob/release/v1.4.1/contracts/libraries/SafeStorage.sol
@@ -23,6 +24,7 @@ library Safe {
2324 error ApiKitUrlNotFound (uint256 chainId );
2425 error MultiSendCallOnlyNotFound (uint256 chainId );
2526 error ArrayLengthsMismatch (uint256 a , uint256 b );
27+ error ProposeTransactionFailed (uint256 statusCode , string response );
2628
2729 struct Instance {
2830 address safe;
@@ -56,28 +58,27 @@ library Safe {
5658 self.instances.push ();
5759 Instance storage i = self.instances[self.instances.length - 1 ];
5860 i.safe = safe;
59- // https://github.com/safe-global/safe-core-sdk/blob/r60/packages/api-kit/src/utils/config.ts
60- i.urls[1 ] = "https://safe-transaction-mainnet.safe.global/api " ;
61- i.urls[10 ] = "https://safe-transaction-optimism.safe.global/api " ;
62- i.urls[56 ] = "https://safe-transaction-bsc.safe.global/api " ;
63- i.urls[100 ] = "https://safe-transaction-gnosis-chain.safe.global/api " ;
64- i.urls[130 ] = "https://safe-transaction-unichain.safe.global/api " ;
65- i.urls[137 ] = "https://safe-transaction-polygon.safe.global/api " ;
66- i.urls[196 ] = "https://safe-transaction-xlayer.safe.global/api " ;
67- i.urls[324 ] = "https://safe-transaction-zksync.safe.global/api " ;
68- i.urls[480 ] = "https://safe-transaction-worldchain.safe.global/api " ;
69- i.urls[1101 ] = "https://safe-transaction-zkevm.safe.global/api " ;
70- i.urls[5000 ] = "https://safe-transaction-mantle.safe.global/api " ;
71- i.urls[8453 ] = "https://safe-transaction-base.safe.global/api " ;
72- i.urls[42161 ] = "https://safe-transaction-arbitrum.safe.global/api " ;
73- i.urls[42220 ] = "https://safe-transaction-celo.safe.global/api " ;
74- i.urls[43114 ] = "https://safe-transaction-avalanche.safe.global/api " ;
75- i.urls[59144 ] = "https://safe-transaction-linea.safe.global/api " ;
76- i.urls[81457 ] = "https://safe-transaction-blast.safe.global/api " ;
77- i.urls[84532 ] = "https://safe-transaction-base-sepolia.safe.global/api " ;
78- i.urls[534352 ] = "https://safe-transaction-scroll.safe.global/api " ;
79- i.urls[11155111 ] = "https://safe-transaction-sepolia.safe.global/api " ;
80- i.urls[1313161554 ] = "https://safe-transaction-aurora.safe.global/api " ;
61+ // https://github.com/safe-global/safe-core-sdk/blob/4d89cb9b1559e4349c323a48a10caf685f7f8c88/packages/api-kit/src/utils/config.ts
62+ i.urls[1 ] = "https://api.safe.global/tx-service/eth/api " ;
63+ i.urls[10 ] = "https://api.safe.global/tx-service/oeth/api " ;
64+ i.urls[56 ] = "https://api.safe.global/tx-service/bnb/api " ;
65+ i.urls[100 ] = "https://api.safe.global/tx-service/gno/api " ;
66+ i.urls[130 ] = "https://api.safe.global/tx-service/unichain/api " ;
67+ i.urls[137 ] = "https://api.safe.global/tx-service/pol/api " ;
68+ i.urls[196 ] = "https://api.safe.global/tx-service/okb/api " ;
69+ i.urls[324 ] = "https://api.safe.global/tx-service/zksync/api " ;
70+ i.urls[480 ] = "https://api.safe.global/tx-service/wc/api " ;
71+ i.urls[1101 ] = "https://api.safe.global/tx-service/zkevm/api " ;
72+ i.urls[5000 ] = "https://api.safe.global/tx-service/mantle/api " ;
73+ i.urls[8453 ] = "https://api.safe.global/tx-service/base/api " ;
74+ i.urls[42161 ] = "https://api.safe.global/tx-service/arb1/api " ;
75+ i.urls[42220 ] = "https://api.safe.global/tx-service/celo/api " ;
76+ i.urls[43114 ] = "https://api.safe.global/tx-service/avax/api " ;
77+ i.urls[59144 ] = "https://api.safe.global/tx-service/linea/api " ;
78+ i.urls[84532 ] = "https://api.safe.global/tx-service/basesep/api " ;
79+ i.urls[534352 ] = "https://api.safe.global/tx-service/scr/api " ;
80+ i.urls[11155111 ] = "https://api.safe.global/tx-service/sep/api " ;
81+ i.urls[1313161554 ] = "https://api.safe.global/tx-service/aurora/api " ;
8182
8283 // https://github.com/safe-global/safe-deployments/blob/v1.37.32/src/assets/v1.3.0/multi_send_call_only.json
8384 i.multiSendCallOnly[1 ] = MultiSendCallOnly (MULTI_SEND_CALL_ONLY_ADDRESS_CANONICAL);
@@ -96,7 +97,6 @@ library Safe {
9697 i.multiSendCallOnly[42220 ] = MultiSendCallOnly (MULTI_SEND_CALL_ONLY_ADDRESS_CANONICAL);
9798 i.multiSendCallOnly[43114 ] = MultiSendCallOnly (MULTI_SEND_CALL_ONLY_ADDRESS_CANONICAL);
9899 i.multiSendCallOnly[59144 ] = MultiSendCallOnly (MULTI_SEND_CALL_ONLY_ADDRESS_CANONICAL);
99- i.multiSendCallOnly[81457 ] = MultiSendCallOnly (MULTI_SEND_CALL_ONLY_ADDRESS_CANONICAL);
100100 i.multiSendCallOnly[84532 ] = MultiSendCallOnly (MULTI_SEND_CALL_ONLY_ADDRESS_CANONICAL);
101101 i.multiSendCallOnly[534352 ] = MultiSendCallOnly (MULTI_SEND_CALL_ONLY_ADDRESS_CANONICAL);
102102 i.multiSendCallOnly[11155111 ] = MultiSendCallOnly (MULTI_SEND_CALL_ONLY_ADDRESS_CANONICAL);
@@ -138,9 +138,8 @@ library Safe {
138138 Enum.Operation operation ,
139139 uint256 nonce
140140 ) internal view returns (bytes32 ) {
141- return ISafeSmartAccount (instance (self).safe).getTransactionHash (
142- to, value, data, operation, 0 , 0 , 0 , address (0 ), address (0 ), nonce
143- );
141+ return ISafeSmartAccount (instance (self).safe)
142+ .getTransactionHash (to, value, data, operation, 0 , 0 , 0 , address (0 ), address (0 ), nonce);
144143 }
145144
146145 // https://github.com/safe-global/safe-core-sdk/blob/r60/packages/api-kit/src/SafeApiKit.ts#L574
@@ -158,14 +157,21 @@ library Safe {
158157 instance (self).requestBody = vm.serializeUint (".proposeTransaction " , "gasPrice " , 0 );
159158 instance (self).requestBody = vm.serializeUint (".proposeTransaction " , "nonce " , params.nonce);
160159
161- instance (self).http.instance ().POST (
162- string .concat (
163- getApiKitUrl (self, block .chainid ),
164- "/v1/safes/ " ,
165- vm.toString (instance (self).safe),
166- "/multisig-transactions/ "
167- )
168- ).withBody (instance (self).requestBody).request ();
160+ HTTP.Response memory response = instance (self).http.instance ()
161+ .POST (
162+ string .concat (
163+ getApiKitUrl (self, block .chainid ),
164+ "/v1/safes/ " ,
165+ vm.toString (instance (self).safe),
166+ "/multisig-transactions/ "
167+ )
168+ ).withBody (instance (self).requestBody).request ();
169+
170+ // The response status should be 2xx, otherwise there was an issue
171+ if (response.status < 200 || response.status >= 300 ) {
172+ revert ProposeTransactionFailed (response.status, response.data);
173+ }
174+
169175 return safeTxHash;
170176 }
171177
@@ -204,6 +210,35 @@ library Safe {
204210 return proposeTransaction (self, params);
205211 }
206212
213+ /// @notice Propose a transaction with a precomputed signature
214+ /// @dev This can be used to propose transactions signed with a hardware wallet in a two-step process
215+ ///
216+ /// @param self The Safe client
217+ /// @param to The target address for the transaction
218+ /// @param data The data payload for the transaction
219+ /// @param sender The address of the account that is proposing the transaction
220+ /// @param signature The precomputed signature for the transaction, e.g. using {sign}
221+ /// @return txHash The hash of the proposed Safe transaction
222+ function proposeTransactionWithSignature (
223+ Client storage self ,
224+ address to ,
225+ bytes memory data ,
226+ address sender ,
227+ bytes memory signature
228+ ) internal returns (bytes32 txHash ) {
229+ ExecTransactionParams memory params = ExecTransactionParams ({
230+ to: to,
231+ value: 0 ,
232+ data: data,
233+ operation: Enum.Operation.Call,
234+ sender: sender,
235+ signature: signature,
236+ nonce: getNonce (self)
237+ });
238+ txHash = proposeTransaction (self, params);
239+ return txHash;
240+ }
241+
207242 function getProposeTransactionsTargetAndData (Client storage self , address [] memory targets , bytes [] memory datas )
208243 internal
209244 view
@@ -246,6 +281,45 @@ library Safe {
246281 return proposeTransaction (self, params);
247282 }
248283
284+ /// @notice Propose multiple transactions with a precomputed signature
285+ /// @dev This can be used to propose transactions signed with a hardware wallet in a two-step process.
286+ /// The signature must be created with Enum.Operation.DelegateCall, as batch transactions use
287+ /// DelegateCall to preserve msg.sender across sub-calls.
288+ ///
289+ /// WARNING: Using Enum.Operation.Call instead of DelegateCall will cause the Safe API to reject
290+ /// your transaction with an error about an incorrect signer address. The signature will be invalid
291+ /// because it was signed with the wrong operation type.
292+ ///
293+ /// @param self The Safe client
294+ /// @param targets The list of target addresses for the transactions
295+ /// @param datas The list of data payloads for the transactions
296+ /// @param sender The address of the account that is proposing the transactions
297+ /// @param signature The precomputed signature for the batch of transactions. MUST be signed with
298+ /// Enum.Operation.DelegateCall (use {sign} with DelegateCall operation).
299+ /// Signing with Call instead of DelegateCall will result in signature validation failure.
300+ /// @return txHash The hash of the proposed Safe transaction
301+ function proposeTransactionsWithSignature (
302+ Client storage self ,
303+ address [] memory targets ,
304+ bytes [] memory datas ,
305+ address sender ,
306+ bytes memory signature
307+ ) internal returns (bytes32 txHash ) {
308+ (address to , bytes memory data ) = getProposeTransactionsTargetAndData (self, targets, datas);
309+ // using DelegateCall to preserve msg.sender across sub-calls
310+ ExecTransactionParams memory params = ExecTransactionParams ({
311+ to: to,
312+ value: 0 ,
313+ data: data,
314+ operation: Enum.Operation.DelegateCall,
315+ sender: sender,
316+ signature: signature,
317+ nonce: getNonce (self)
318+ });
319+ txHash = proposeTransaction (self, params);
320+ return txHash;
321+ }
322+
249323 function getExecTransactionData (Client storage self , address to , bytes memory data , address sender )
250324 internal
251325 returns (bytes memory )
@@ -322,15 +396,25 @@ library Safe {
322396 );
323397 }
324398
399+ /// @notice Prepare the signature for a transaction, using a custom nonce
400+ ///
401+ /// @param self The Safe client
402+ /// @param to The target address for the transaction
403+ /// @param data The data payload for the transaction
404+ /// @param operation The operation to perform
405+ /// @param sender The address of the account that is signing the transaction
406+ /// @param nonce The nonce of the transaction
407+ /// @param derivationPath The derivation path for the transaction
408+ /// @return signature The signature for the transaction
325409 function sign (
326410 Client storage self ,
327411 address to ,
328412 bytes memory data ,
329413 Enum.Operation operation ,
330414 address sender ,
415+ uint256 nonce ,
331416 string memory derivationPath
332417 ) internal returns (bytes memory ) {
333- uint256 nonce = getNonce (self);
334418 if (bytes (derivationPath).length > 0 ) {
335419 string [] memory inputs = new string [](8 );
336420 inputs[0 ] = "cast " ;
@@ -355,12 +439,25 @@ library Safe {
355439 vm.toString (nonce),
356440 ',"safeTxGas":"0"},"primaryType":"SafeTx","types":{"SafeTx":[{"name":"to","type":"address"},{"name":"value","type":"uint256"},{"name":"data","type":"bytes"},{"name":"operation","type":"uint8"},{"name":"safeTxGas","type":"uint256"},{"name":"baseGas","type":"uint256"},{"name":"gasPrice","type":"uint256"},{"name":"gasToken","type":"address"},{"name":"refundReceiver","type":"address"},{"name":"nonce","type":"uint256"}]}} '
357441 );
442+ /// forge-lint: disable-next-line(unsafe-cheatcode)
358443 bytes memory output = vm.ffi (inputs);
359444 return output;
360445 } else {
361446 Signature memory sig;
362- (sig.v, sig.r, sig.s) = vm.sign (sender, getSafeTxHash (self, to, 0 , data, Enum.Operation.Call , nonce));
447+ (sig.v, sig.r, sig.s) = vm.sign (sender, getSafeTxHash (self, to, 0 , data, operation , nonce));
363448 return abi.encodePacked (sig.r, sig.s, sig.v);
364449 }
365450 }
451+
452+ /// @notice Prepare the signature for a transaction, using the nonce from the Safe
453+ function sign (
454+ Client storage self ,
455+ address to ,
456+ bytes memory data ,
457+ Enum.Operation operation ,
458+ address sender ,
459+ string memory derivationPath
460+ ) internal returns (bytes memory ) {
461+ return sign (self, to, data, operation, sender, getNonce (self), derivationPath);
462+ }
366463}
0 commit comments