Skip to content

Commit 2ca553e

Browse files
chore: added safe (create2 via CreateX) and ignition (create) deployment scripts (#4)
* chore: added safe (create2 via CreateX) and ignition (create) deployment scripts
1 parent b479e30 commit 2ca553e

9 files changed

Lines changed: 198 additions & 2 deletions

File tree

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,13 @@ constructor(
6262
)
6363
```
6464

65+
## Acknowledgments
66+
67+
- Smart contracts: [Solidity](https://soliditylang.org)
68+
- Permit standards: [EIP-2612](https://eips.ethereum.org/EIPS/eip-2612), [Permit2](https://github.com/Uniswap/permit2)
69+
- Request signing: [EIP-712](https://eips.ethereum.org/EIPS/eip-712)
70+
- Relayer infrastructure: [Openzeppelin Relayer](https://github.com/OpenZeppelin/openzeppelin-relayer)
71+
6572
## License
6673

6774
MIT License - Copyright (c) 2025 Cosine Labs Inc.

ignition/README.md

Lines changed: 0 additions & 1 deletion
This file was deleted.

ignition/modules/Handoff.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { buildModule } from '@nomicfoundation/hardhat-ignition/modules';
2+
3+
const HandoffModule = buildModule('HandoffModule', (instance) => {
4+
const SAFE_ADDRESS = process.env.SAFE_ADDRESS as string;
5+
const Handoff = instance.contract('Handoff', [
6+
['0xFcDcD01BCaB08C9551Dd87eF552f3916F9875b12', '0xf3207f8BB31c9c27FaBebFFaeb69F060BfC37171'],
7+
'0x000000000022D473030F116dDEE9F6B43aC78BA3',
8+
SAFE_ADDRESS
9+
]);
10+
return { Handoff };
11+
});
12+
13+
export default HandoffModule;

scripts/README.md

Lines changed: 0 additions & 1 deletion
This file was deleted.

scripts/safe/deploy.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { ethers } from 'hardhat';
2+
import SafeApiKit from '@safe-global/api-kit';
3+
import Safe from '@safe-global/protocol-kit';
4+
import { OperationType } from '@safe-global/types-kit';
5+
import { deployViaCreateX } from './utils/deployViaCreateX';
6+
import { getTransactionReceipt, stripAddressPadding } from './utils/helpers';
7+
8+
/**
9+
* Deployment script
10+
*/
11+
async function deploy() {
12+
const [signer] = await ethers.getSigners();
13+
14+
const CHAIN_ID = BigInt(process.env.CHAIN_ID as string);
15+
const CHAIN_RPC_URL = process.env.CHAIN_RPC_URL as string;
16+
const PRIVATE_KEY = process.env.PRIVATE_KEY as string;
17+
const SAFE_ADDRESS = process.env.SAFE_ADDRESS as string;
18+
const SAFE_API_KEY = process.env.SAFE_API_KEY as string;
19+
20+
const protocolKit = await Safe.init({
21+
provider: CHAIN_RPC_URL,
22+
signer: PRIVATE_KEY,
23+
safeAddress: SAFE_ADDRESS
24+
});
25+
const apiKit = new SafeApiKit({
26+
chainId: CHAIN_ID,
27+
apiKey: SAFE_API_KEY
28+
});
29+
30+
const trx = await deployViaCreateX();
31+
const safeTransaction = await protocolKit.createTransaction({
32+
transactions: [{ ...trx, value: '0', operation: OperationType.Call }]
33+
});
34+
const safeTxHash = await protocolKit.getTransactionHash(safeTransaction);
35+
const signature = await protocolKit.signHash(safeTxHash);
36+
37+
await apiKit.proposeTransaction({
38+
safeAddress: SAFE_ADDRESS,
39+
safeTransactionData: safeTransaction.data,
40+
safeTxHash,
41+
senderAddress: signer.address,
42+
senderSignature: signature.data
43+
});
44+
45+
const threshold = await protocolKit.getThreshold();
46+
const confirmations = await apiKit.getTransactionConfirmations(safeTxHash);
47+
console.log(`Current signatures: ${confirmations.count}, Required: ${threshold}`);
48+
49+
if (confirmations.count >= threshold) {
50+
const safeTransaction = await apiKit.getTransaction(safeTxHash);
51+
const { hash } = await protocolKit.executeTransaction(safeTransaction);
52+
const receipt = await getTransactionReceipt(hash);
53+
const createX = receipt?.logs.find((log: any) => log.address.toLowerCase() == trx.to.toLowerCase());
54+
const deployedAt = createX?.topics[1];
55+
if (deployedAt) {
56+
console.log('Transaction hash: ', hash);
57+
console.log('Contract successfully deployed at: ', stripAddressPadding(deployedAt));
58+
}
59+
} else {
60+
console.log(`Waiting for atleast ${threshold - confirmations.count} more signature(s) before execution`);
61+
console.log(`Share this transaction hash with other Safe owners: ${safeTxHash}`);
62+
}
63+
}
64+
65+
deploy()
66+
.then(() => process.exit(0))
67+
.catch((error) => {
68+
console.error('Deployment failed:');
69+
console.error(error);
70+
process.exit(1);
71+
});
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { ethers } from 'hardhat';
2+
import { getDeployment } from './getDeployment';
3+
4+
/**
5+
* Get deployment data and constructor arguments
6+
* See CreateX: https://createx.rocks/deployments
7+
*/
8+
export const deployViaCreateX = async () => {
9+
const CreateXFactory = '0xba5Ed099633D3B313e4D5F7bdc1305d3c28ba5Ed';
10+
const SAFE_ADDRESS = process.env.SAFE_ADDRESS as string;
11+
const { data } = await getDeployment();
12+
const CreateXABI = [
13+
{
14+
inputs: [
15+
{ internalType: 'bytes32', name: 'salt', type: 'bytes32' },
16+
{ internalType: 'bytes', name: 'initCode', type: 'bytes' }
17+
],
18+
name: 'deployCreate2',
19+
outputs: [{ internalType: 'address', name: 'newContract', type: 'address' }],
20+
stateMutability: 'payable',
21+
type: 'function'
22+
}
23+
];
24+
const iface = new ethers.Interface(CreateXABI);
25+
const encodedData = iface.encodeFunctionData('deployCreate2', [
26+
ethers.solidityPacked(['address', 'uint8', 'bytes11'], [SAFE_ADDRESS, 0x00, '0xa10f771643cef3d62934f1']),
27+
data
28+
]);
29+
return { to: CreateXFactory, data: encodedData };
30+
};
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { ethers } from 'hardhat';
2+
3+
/**
4+
* Get deployment data and constructor arguments
5+
*/
6+
export const getDeployment = async () => {
7+
const SAFE_ADDRESS = process.env.SAFE_ADDRESS as string;
8+
const Handoff = await ethers.getContractFactory('Handoff');
9+
const args = {
10+
relayers: ['0xFcDcD01BCaB08C9551Dd87eF552f3916F9875b12', '0xf3207f8BB31c9c27FaBebFFaeb69F060BfC37171'],
11+
permit2: '0x000000000022D473030F116dDEE9F6B43aC78BA3',
12+
owner: SAFE_ADDRESS
13+
};
14+
const { data } = await Handoff.getDeployTransaction(args.relayers, args.permit2, args.owner);
15+
return { data, args };
16+
};

scripts/safe/utils/helpers.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { ethers } from 'hardhat';
2+
3+
/**
4+
* Pauses execution for the specified number of milliseconds.
5+
*/
6+
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
7+
8+
/**
9+
* Strips the 12-byte zero-padding from a 32-byte ABI-encoded address such as those found in transaction logs.
10+
* E.g:
11+
* Input: 0x00000000000000000000000002831ae5c600cfbf3fb5a9521313868f546a7ea4
12+
* Output: 0x02831aE5C600cFBF3FB5a9521313868F546A7ea4
13+
*/
14+
export const stripAddressPadding = (address: string) => {
15+
const rawAddress = '0x' + address.slice(26);
16+
return ethers.getAddress(rawAddress);
17+
};
18+
19+
/**
20+
* Repeatedly fetches a transaction receipt until it is found or the maximum number of retries is reached.
21+
*/
22+
export const getTransactionReceipt = async (hash: string, retries = 15): Promise<any> => {
23+
try {
24+
const receipt = await ethers.provider.getTransactionReceipt(hash);
25+
if (!receipt || receipt?.logs?.length == 0) {
26+
if (retries <= 0) throw new Error(`Max retries reached for ${hash}`);
27+
await sleep(5000);
28+
return getTransactionReceipt(hash, retries - 1);
29+
}
30+
return receipt;
31+
} catch (error: any) {
32+
if (error.name === 'TransactionReceiptNotFoundError') {
33+
if (retries <= 0) throw new Error(`Max retries reached for ${hash}`);
34+
await sleep(5000);
35+
return getTransactionReceipt(hash, retries - 1);
36+
}
37+
throw error;
38+
}
39+
};

scripts/safe/verify.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { run } from 'hardhat';
2+
import { getDeployment } from './utils/getDeployment';
3+
4+
/**
5+
* Verification script
6+
*/
7+
async function verify() {
8+
const DEPLOYED_ADDRESS = process.env.DEPLOYED_ADDRESS;
9+
const { args } = await getDeployment();
10+
await run('verify:verify', {
11+
address: DEPLOYED_ADDRESS,
12+
constructorArguments: [args.relayers, args.permit2, args.owner]
13+
});
14+
}
15+
16+
verify()
17+
.then(() => process.exit(0))
18+
.catch((error) => {
19+
console.error('Verification failed:');
20+
console.error(error);
21+
process.exit(1);
22+
});

0 commit comments

Comments
 (0)