Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 61 additions & 12 deletions .github/workflows/base-mainnet-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ on:
workflow_dispatch:
inputs:
confirm:
description: 'Type DEPLOY to authorize mainnet deployment'
description: 'Type DEPLOY to authorize Base mainnet deployment'
required: true
default: ''

Expand All @@ -27,23 +27,72 @@ jobs:
node-version: 24
cache: npm

- name: Install
run: cd base-agent && npm ci
- name: Install dependencies
run: npm ci

- name: Preflight Base mainnet signer and RPC
env:
BASE_RPC_URL: ${{ secrets.BASE_RPC_URL }}
PRIVATE_KEY: ${{ secrets.DEPLOYER_PRIVATE_KEY || secrets.PRIVATE_KEY }}
run: |
node <<'NODE'
const { JsonRpcProvider, Wallet, formatEther } = require('ethers');

async function main() {
const rpc = process.env.BASE_RPC_URL;
const key = process.env.PRIVATE_KEY;
if (!rpc) throw new Error('BASE_RPC_URL secret is required');
if (!key) throw new Error('DEPLOYER_PRIVATE_KEY or PRIVATE_KEY secret is required');

const provider = new JsonRpcProvider(rpc);
const network = await provider.getNetwork();
if (network.chainId !== 8453n) {
throw new Error(`Refusing deployment: expected Base mainnet chainId 8453, got ${network.chainId}`);
}

const wallet = new Wallet(key, provider);
const balance = await provider.getBalance(wallet.address);
console.log(`Base mainnet chainId verified: ${network.chainId}`);
console.log(`Deployer address: ${wallet.address}`);
console.log(`Deployer balance: ${formatEther(balance)} ETH`);
if (balance === 0n) throw new Error('Deployer has no ETH for gas');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Compare the signer balance with the deployment cost

When the deployer has any dust balance greater than zero but less than the deployment transaction's gas cost, this preflight succeeds even though the gated mainnet deployment is guaranteed to fail with insufficient funds. The check should estimate the AgentExecutor deployment gas and account for current fee data, or enforce a conservative minimum balance, rather than treating every nonzero balance as gas-ready.

Useful? React with 👍 / 👎.

}

main().catch((error) => {
console.error(error.message || error);
process.exit(1);
});
NODE

- name: Compile
run: cd base-agent && npm run compile
run: npm run compile

- name: Test
run: cd base-agent && npm test
run: npm test

- name: Deploy Base Mainnet
- name: Deploy AgentExecutor to Base mainnet
env:
BASE_RPC_URL: ${{ secrets.BASE_RPC_URL }}
PRIVATE_KEY: ${{ secrets.PRIVATE_KEY }}
BASESCAN_API_KEY: ${{ secrets.BASESCAN_API_KEY }}
run: cd base-agent && npm run deploy:base
PRIVATE_KEY: ${{ secrets.DEPLOYER_PRIVATE_KEY || secrets.PRIVATE_KEY }}
DESTINATION_ADDRESS: ${{ secrets.DESTINATION_ADDRESS }}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Record the contract's actual fixed destination

The root deploy script merely copies this environment value into deployment-base.json, while contracts/AgentExecutor.sol uses the compiled-in DESTINATION constant and never reads DESTINATION_ADDRESS. Consequently, when this undocumented secret is absent the uploaded record says destination: null, and when it differs it records an address to which the deployed contract can never withdraw; derive the field from the deployed contract or validate the secret against its constant.

Useful? React with 👍 / 👎.

run: npm run deploy:base

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Deploy with the intended permanent owner

When DEPLOYER_PRIVATE_KEY belongs to the separately recommended deployment wallet, AgentExecutor assigns that signer as owner through owner = msg.sender, even though docs/OWNER_POLICY.md specifies 0xfd1610f5eae31dd757e55d6b4ba543b80a2720b3 as the permanent owner and the contract has no ownership-transfer function. This mainnet deployment therefore leaves the deployer key with permanent executor-administration authority; either construct the contract with the intended owner or reject a signer whose address is not the required owner.

Useful? React with 👍 / 👎.


- name: Capture deployment address
run: |
test -f deployment-base.json
CONTRACT_ADDRESS="$(node -p "require('./deployment-base.json').address")"
test -n "$CONTRACT_ADDRESS"
echo "CONTRACT_ADDRESS=$CONTRACT_ADDRESS" >> "$GITHUB_ENV"
echo "Deployed AgentExecutor: $CONTRACT_ADDRESS"

- name: Verify Deployment
- name: Verify on BaseScan
env:
CONTRACT_ADDRESS: ${{ secrets.CONTRACT_ADDRESS }}
run: cd base-agent && node tasks/verify.js
BASE_RPC_URL: ${{ secrets.BASE_RPC_URL }}
ETHERSCAN_API_KEY: ${{ secrets.BASESCAN_API_KEY }}
run: npx hardhat verify --network base "$CONTRACT_ADDRESS"

- name: Upload deployment record
uses: actions/upload-artifact@v4
Comment on lines +94 to +95

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve the deployment record before verification

If BaseScan verification fails—for example because its API key is missing/invalid or the new contract has not been indexed yet—the upload step is skipped by the workflow's implicit success() condition, even though the preceding mainnet deployment is already irreversible. Upload deployment-base.json before verification or give this step an if: always() guard so every successful broadcast retains its structured deployment record.

Useful? React with 👍 / 👎.

with:
name: deployment-base-mainnet
path: deployment-base.json
Loading