Documentation
Getting Started
SafeScript lets you write TypeScript scripts that generate Safe batch transactions. Instead of manually encoding function calls, you write readable code with full type safety.
1. Add a Safe wallet
Go to Settings → Safes and add your Safe wallet address. Select which chains the Safe is deployed on.
2. Add your contracts
Go to Settings → Contracts and add contracts by address. The ABI is automatically fetched from the block explorer. You can also add contract templates (ABI-only) for interacting with any address using .at(address).
3. Add your tokens
Add ERC20 tokens in Settings → Tokens. Tokens get special helpers like .amount() for converting human-readable amounts.
4. Add constants (optional)
Define reusable values like addresses, amounts, or strings in Settings → Constants.
5. Write your script
Use the editor to write TypeScript. Call contract methods with await to add them to the transaction batch.
6. Export and execute
Click "Run" to execute your script. Download the JSON file and import it into Safe Transaction Builder to execute.
Available Variables
safe
The selected Safe wallet. Contains address and chainId.
safe.address // "0x..."
safe.chainId // 1contracts / c
Your contracts. Use contracts.ContractName or the shorthand c.ContractName. Read functions return values directly. Write functions add to the batch when awaited.
// Read a value
const balance = await c.Vault.balanceOf(safe.address)
// Write to batch (use await)
await c.Vault.deposit(amount, safe.address)
// Templates - use .at(address) to specify target
await c.ERC20.at("0x...").approve(spender, amount)tokens / t
Your ERC20 tokens. Use tokens.name or shorthand t.name. Tokens have all ERC20 methods plus helpers.
// Convert human amount to wei using .amount()
const amount = t.usdc.amount(1000) // 1000 USDC
const half = t.usdc.amount(0.5) // 0.5 USDC
const big = t.usdc.amount(1000000n) // 1M USDC
// Token properties
t.usdc.address // "0x..." (on current chain)
t.usdc.decimals // 6
t.usdc.symbol // "USDC"
// ERC20 methods
await t.usdc.approve(spender, amount)
await t.usdc.transfer(recipient, amount)
const balance = await t.usdc.balanceOf(safe.address)constants / k
Your custom constants plus built-ins. Use constants.name or shorthand k.name.
// Custom constants (defined in Settings)
k.treasury // "0x..." (address type)
k.maxAmount // 1000n (bigint type)
k.apiEndpoint // "https://..." (string type)
// Built-in constants
k.MaxUint256 // Max uint256 value
k.ZeroAddress // 0x0000...0000form / f
Form field values filled in by the user. Use form.fieldName or shorthand f.fieldName. Values are typed based on the field definition: text/address/select return strings, number returns a number, boolean returns true/false.
// Access form values
const recipient = f.recipientAddress // string
const amount = f.amount // number
const shouldClaim = f.claimRewards // boolean
await t.usdc.transfer(recipient, t.usdc.amount(amount))
if (shouldClaim) {
await c.Vault.claimRewards(safe.address)
}console
Log values to the output panel for debugging.
console.log("Amount:", amount)
console.log("Safe address:", safe.address)
console.error("Something went wrong")
console.info("Transaction added")Built-in Contract Templates
These standard contract interfaces are always available. Use .at(address) to interact with any contract.
ERC20
// Interact with any ERC20 token
const token = c.ERC20.at("0x...")
await token.approve(spender, amount)
await token.transfer(recipient, amount)
const balance = await token.balanceOf(safe.address)ERC721
// Interact with any NFT
const nft = c.ERC721.at("0x...")
await nft.transferFrom(safe.address, recipient, tokenId)
await nft.approve(operator, tokenId)
await nft.setApprovalForAll(operator, true)ERC4626
// Interact with any ERC4626 vault
const vault = c.ERC4626.at("0x...")
await vault.deposit(assets, safe.address)
await vault.withdraw(assets, recipient, safe.address)
const shares = await vault.balanceOf(safe.address)Parallel Reads with Multicall
Use c.multicall() to fetch multiple values in parallel.
// Array mode - returns array of results
const [balance, allowance, totalSupply] = await c.multicall([
t.usdc.balanceOf(safe.address),
t.usdc.allowance(safe.address, spender),
t.usdc.totalSupply(),
])
// Object mode - returns object with same keys
const data = await c.multicall({
balance: t.usdc.balanceOf(safe.address),
allowance: t.usdc.allowance(safe.address, spender),
})
console.log(data.balance, data.allowance)Form Fields
Add form fields to your scripts so users can fill in values before running. This is useful for creating reusable scripts that non-technical team members can operate without touching the code.
Setting up form fields
Click the Form button in the script toolbar to open the field editor. Each field has a name (used as the variable in your script), a label, and a type.
Available field types:
- Text — free-form string input
- Number — numeric input, parsed as a number
- Boolean — checkbox, returns true/false
- Address — text input styled for Ethereum addresses
- Select — dropdown with predefined options
Fields can be marked as required, have default values, placeholders, and descriptions. The Run button is disabled until all required fields are filled in.
Using form values in scripts
// Form fields are available as form.name or f.name
// with full IntelliSense based on your field definitions
const amount = t.usdc.amount(f.amount)
await t.usdc.transfer(f.recipient, amount)
if (f.approveMax) {
await t.usdc.approve(f.spender, k.MaxUint256)
}Form view
Scripts with form fields have a Form View button in the toolbar. This opens a simplified view at /org/your-org/form/script-id that shows only the form, Safe selector, Run button, and output — no code editor. Share this URL with team members who need to run scripts without seeing the code.
Examples
Token Approval + Deposit
// Approve and deposit USDC into a vault
const amount = t.usdc.amount(10000) // 10,000 USDC
await t.usdc.approve(c.Vault.address, amount)
await c.Vault.deposit(amount, safe.address)
console.log("Deposited", amount, "USDC")Multi-send to Multiple Recipients
// Send tokens to multiple addresses
const recipients = [
{ address: "0x111...", amount: t.usdc.amount(100) },
{ address: "0x222...", amount: t.usdc.amount(200) },
{ address: "0x333...", amount: t.usdc.amount(300) },
]
for (const r of recipients) {
await t.usdc.transfer(r.address, r.amount)
}
console.log("Sent to", recipients.length, "recipients")Conditional Logic with Reads
// Check allowance before approving
const amount = t.usdc.amount(5000)
const allowance = await t.usdc.allowance(safe.address, c.Vault.address)
if (allowance < amount) {
console.log("Approving...")
await t.usdc.approve(c.Vault.address, k.MaxUint256)
}
await c.Vault.deposit(amount, safe.address)Working with Any Token Address
// Use ERC20 template for tokens not in your list
const randomToken = c.ERC20.at("0xabc...")
const balance = await randomToken.balanceOf(safe.address)
console.log("Balance:", balance)
await randomToken.transfer(k.treasury, balance)Ready to start building?
Sign up for free and create your first script.