CalixMultisig is an M-of-N on-chain multisig wallet used to govern the Bridge, Oracle, and LPStaking contracts. All governance changes â including ownership of the multisig itself â go through the same proposal lifecycle.
The contract enforces a submit â approve â execute lifecycle. A proposal is a raw (target, value, calldata) tuple. Once it accumulates threshold approvals from distinct owners, any owner can execute it. Execution is atomic: if the call reverts, the proposal stays pending.
addOwner, removeOwner, and changeThreshold can only be called via a successful multisig proposal â never directly. The multisig is its own admin.const data = iface.encodeFunctionData("setRewardRate", [newRate]);
const txId = await multisig.submit(stakingAddress, 0, data);
await multisig.connect(owner2).approve(txId);
await multisig.execute(txId);
| Function | Access | Description |
|---|---|---|
submit(address, uint256, bytes) | owner | Create a new proposal; returns proposal ID |
approve(uint256 txId) | owner | Add caller's approval to a pending proposal |
revoke(uint256 txId) | owner | Withdraw caller's approval before execution |
execute(uint256 txId) | owner | Execute a proposal that has reached threshold |
isReady(uint256 txId) | public | Returns true if approval count ≥ threshold |
getProposal(uint256 txId) | public | Returns target, value, data, executed flag, approval count |
Use Solidity's abi.encodeWithSignature for calldata in tests/scripts:
// Solidity
bytes memory data = abi.encodeWithSignature(
"transferOwnership(address)",
newOwner
);
// ethers.js
const iface = new ethers.Interface(["function transferOwnership(address)"]);
const data = iface.encodeFunctionData("transferOwnership", [newOwner]);
nonReentrant on execute â prevents re-entrant proposal executiononlySelf modifier on addOwner, removeOwner, changeThreshold â only callable through a successful proposal