Guidelines for working with this codebase.
Always respond in Brazilian Portuguese (pt-BR).
Always read README.md at the project root before starting any task.
execution-layer/
├── contracts/
│ ├── aerodrome/ # Base: PanoramaExecutorV2, AerodromeAdapterV2, DCAVault
│ └── avax/ # Avalanche: TraderJoeAdapter, BenqiLendAdapter, SAVAXAdapter
├── backend/ # Node.js/TypeScript — Express API
├── script/ # Foundry deploy scripts (V1 + V2)
├── test/ # Foundry tests (unit + fork)
└── frontend/ # Demo UI
# Solidity unit tests (no RPC needed)
forge test -vv --no-match-path "test/fork/*"
# Fork tests (requires BASE_RPC_URL)
BASE_RPC_URL=https://mainnet.base.org forge test --match-path "test/fork/*" -vvv
# Backend (Vitest)
cd backend && npm testAlways run both suites after any change. Do not commit with failing tests.
The system uses BeaconProxy (OpenZeppelin) instead of EIP-1167:
- Each protocol has an
UpgradeableBeaconthat stores the implementation address - Each user gets a
BeaconProxythat delegates to the beacon beacon.upgradeTo(newImpl)upgrades ALL users at once- Adapters use
Initializable+__gap[50]for storage stability
function execute(
bytes32 protocolId,
bytes4 action, // bytes4(keccak256("functionName(types...)"))
Transfer[] calldata transfers,
uint256 deadline,
bytes calldata data
) external payable returns (bytes memory result)The executor does not know any specific action. It only:
- Creates/retrieves the user's BeaconProxy for
protocolId - Pulls tokens from the user to the proxy via
transfers - Calls
proxy.call(action ++ data)— blind dispatch
Never add action-specific logic to the executor. All logic goes in the adapter.
// on-chain
executor.registerBeacon(keccak256("aerodrome"), beaconAddress, abi.encode(router, voter));// backend
registerProtocol("aerodrome", { protocolId: "aerodrome", chain: "base", ... });Zero changes needed in the executor or BundleBuilder.
All V2 adapters use the same signature:
function initializeFull(address _executor, bytes calldata _initArgs) external initializerThe executor stores protocolInitArgs per protocol and passes them to initializeFull when creating the proxy.
The selectors in backend/src/shared/bundle-builder.ts use the full signature:
ethers.id("swap(address,address,uint256,uint256,address,bool)").slice(0, 10)Do not use ethers.id("swap") — that is keccak256 of the name without types.
new BundleBuilder(chainId)
.addApproveIfNeeded(token, spender, currentAllowance, required, "Approve X")
.addExecute(protocolId, ADAPTER_SELECTORS.SWAP, transfers, deadline, adapterData, 0n, executor, "Swap")
.build("summary")Never construct PreparedTransaction manually outside of BundleBuilder.
The data passed to execute() must be exactly the abi.encode of the adapter function's typed parameters, without the selector:
const adapterData = ethers.AbiCoder.defaultAbiCoder().encode(
["address", "address", "uint256", "uint256", "address", "bool"],
[tokenIn, tokenOut, amountIn, amountOutMin, recipient, stable]
);Each product has its own module in backend/src/modules/<name>/:
usecases/— business logic, builds bundlescontrollers/— HTTP request/response parsingroutes/— registers Express routes
modules/swap/— Aerodrome swapmodules/liquid-staking/— Aerodrome gaugesmodules/dca/— DCAVault automation
modules/avax-swap/— Trader Joe V1modules/avax-lending/— Benqi Finance
PanoramaExecutorV2.sol: never add action-specific functions. The genericexecute()is the only entry point.- V2 Adapters: always use
Initializable,onlyExecutor,__gap[50],receive() external payable. DCAVault.sol: uses theIPanoramaExecutorinterface (same signature V1/V2).- Storage layout: never reorder storage variables in upgrades. Only append at the end and reduce
__gap.
| Chain | Status | Protocols |
|---|---|---|
| Base (8453) | Active | Aerodrome Finance |
| Avalanche (43114) | Active | Trader Joe, Benqi, sAVAX |
The backend uses getChainConfig("base") or getChainConfig("avalanche") from config/chains.ts.
vi.mock() is hoisted by Vitest. Variables referenced inside the factory must be declared with vi.hoisted():
const { mockFn } = vi.hoisted(() => ({ mockFn: vi.fn() }));
vi.mock("../../some/module", () => ({ myFunc: mockFn }));