Skip to content
Draft
Show file tree
Hide file tree
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
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Futarchy Autonomous Optimizer (FAO)

The [agent-work v1 specification](docs/agent-work-v1.md) defines the ownerless publication index,
canonical task/receipt/payment documents, and their exact binding to typed treasury transfers.
Its P1 reference agent recomputes finalized state on every tick and keeps signing behind an
injected boundary; `python3 tools/agent_anvil_drill.py` regenerates the 16-drill evidence matrix.

This repository contains the smart contracts for the Futarchy Autonomous Optimizer token (FAO) and its sale mechanics. The codebase is implemented with [Foundry](https://book.getfoundry.sh/) and relies on OpenZeppelin libraries for security-reviewed primitives.

## Contracts
Expand Down Expand Up @@ -136,6 +141,14 @@ Override `ECONOMIC_METADATA_BUNDLE`, `EXPECTED_DEPLOYER`, and `EXPECTED_DEPLOYER
for a different deployment. The bundle must describe the release strategy derived from that
deployer and nonce.

Economic deployment manifests use schema v4. Both broadcast and finalized-chain reconstruction
pin the exact live runtime hashes of the vault, proposal gateway, arbitration, and treasury
executor; RPC verification rejects any substituted authority contract before trusting its views.
Schema v4 keeps integer fields as JSON numbers. Browser clients must not recompute full core or FLM
config preimages through ordinary `JSON.parse`, which loses integers above 2^53; they instead root
disclosed hashes in canonical registrar/receipt provenance and verify finalized wiring plus the v4
runtime hashes. The Python verifier remains the lossless full-preimage verifier.

## Gnosis Liquidity Stack Deploy

Deploy `FutarchyOfficialProposalSource`, two Swapr adapters (spot/conditional), and `FutarchyLiquidityManager`:
Expand Down
112 changes: 112 additions & 0 deletions docs/agent-work-v1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# FAO agent-work documents v1

Lane 5 records work and proposed payment without adding a second governance or custody system.
The reviewed `TransferAction` remains the only payment primitive. The optional `AgentWorkIndex`
is one ownerless, storage-free event log shared by every FAO on a chain; economics never read it.

## Publication

```solidity
publish(bytes32 kind, bytes32 parentDigest, bytes document) returns (bytes32 documentDigest)
```

`documentDigest` is `keccak256(document)`. `Published` indexes the kind, parent, and digest and
carries the publisher and exact document bytes. The contract accepts every kind and every nonempty
byte string. It has no owner, allowlist, size cap, deduplication, custody, withdrawal, endorsement,
or payment authority. Gas is the spam bound; clients filter and deduplicate. Earliest finalized log
ordering is only evidence for humans, challengers, and markets.

The v1 kind values are:

- `keccak256("FAO_AGENT_TASK_V1")`
- `keccak256("FAO_AGENT_RECEIPT_V1")`
- `keccak256("FAO_AGENT_PAYMENT_V1")`

## Canonical JSON bytes

Documents are UTF-8 without a BOM and contain one top-level object. Arrays keep their given order.
Object keys are sorted by Unicode codepoint. There is no insignificant whitespace. Every scalar
leaf is a JSON string: numbers, booleans, and null are forbidden, and an absent optional value means
the key is omitted.

Strings escape only quote, backslash, and U+0000 through U+001F. Every control is the lowercase
six-byte form `\u00xx`, including newline and tab; non-ASCII text is raw UTF-8. Unpaired surrogates
are invalid. Addresses are lowercase 20-byte hex, digests are lowercase 32-byte hex, and integers
are unsigned decimal strings without signs or leading zeroes. The tools normalize semantic builder
input, but validators reject exact event bytes whose schema values are not already canonical.

Raw bytes are authoritative: their digest is always Keccak-256. Canonicalization is required for
the three known schemas, not for arbitrary future index kinds.

## Schemas and lineage

A task uses `parentDigest = bytes32(0)`:

```json
{"v":"1","kind":"fao.task","chainId":"11155111","vault":"0x…","title":"…","spec":"…","salt":"0x…"}
```

It must contain either `spec`, or both `specDigest` and advisory `specUri`. `deadline` is optional.
`reward:{"asset":"0x…","amount":"…"}` is optional and advisory; publication creates no bounty
or obligation.

A receipt uses `parentDigest = taskDigest`:

```json
{"v":"1","kind":"fao.receipt","chainId":"…","vault":"0x…","task":"0x…","worker":"0x…","artifacts":[{"digest":"0x…","uri":"…"}],"summary":"…","salt":"0x…"}
```

Each artifact commits to the exact artifact bytes with Keccak-256. Its URI is advisory and limited
to 256 UTF-8 bytes by the v1 tools; `note` is optional. No CID or IPFS dependency exists.
The on-chain kind is permanently `FAO_AGENT_RECEIPT_V1`. “Submission” is only explanatory prose
for this work receipt; it is not a fourth document kind.

A payment envelope uses `parentDigest = receiptDigest`:

```json
{"v":"1","kind":"fao.payment","chainId":"…","vault":"0x…","asset":"0x…","recipient":"0x…","amount":"…","task":"0x…","receipt":"0x…","salt":"0x…"}
```

`note` is optional. The transfer is exactly
`{asset, recipient, amount, salt: keccak256(envelopeBytes)}`. Envelope chain, vault, asset,
recipient, and amount must match the proposed action. The envelope's own salt permits a fresh
identity when rejected or expired economics are proposed again. The resulting transfer hash is the
proposal ID and remains chain- and vault-domain-separated by `FAOTreasuryActions`.

## Payment meaning

Acceptance means authorized, executable while funded, and never partially paid. Funds are neither
reserved nor escrowed: ragequit, buyback, or another action may consume them before execution. A
shortfall reverts atomically and can be retried before expiry. There are no v1 bids, guaranteed
bounties, slashing, exclusive winners, copying rules, or self-payment exceptions. Every proposer,
worker, and recipient goes through the same immutable transfer limits and futarchy path.

The dependency-free implementations and shared golden vectors are
[`tools/agent_documents.py`](../tools/agent_documents.py) and
[`tools/fixtures/agent-document-golden.json`](../tools/fixtures/agent-document-golden.json).
The separate singleton build, runtime hash, CREATE2 salt, and predicted address are pinned in
[`metadata/agent-work-index.json`](../metadata/agent-work-index.json); verify them with
`python3 tools/agent_work_index_code_hashes.py --check`.
The address is a CREATE2 prediction, not deployment evidence. A pinned runtime hash or predicted
address must never be presented as deployed until a chain receipt and matching live code prove it.

## Stateless reference agent

[`tools/agent_runner.py`](../tools/agent_runner.py) is the dependency-free P1 reference agent. Each
tick starts from finalized logs and views, chooses at most one action, simulates and cap-checks it,
then passes the unsigned transaction to an injected sender. It holds no key or authoritative local
state. Accepted, executable, and paid are deliberately separate: acceptance needs a matching view
and finalized arbitration log; executability needs a live queue window and successful exact
`eth_call`; payment needs the exact execution log and conserved balance deltas.

The executable unit, fake-RPC, Anvil, and Sepolia-fork matrix is:

```sh
python3 -m unittest tools.test_agent_documents tools.test_agent_runner
python3 tools/agent_anvil_drill.py
```

The latter regenerates
[`metadata/agent-work-p1-evidence.json`](../metadata/agent-work-p1-evidence.json) and its SHA-256
sidecar. These are house-wallet engineering fixtures. They do not prove a live index deployment,
live payment, useful work, external demand, or guaranteed payment.
20 changes: 20 additions & 0 deletions metadata/agent-work-index.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"schemaVersion": 1,
"source": "src/AgentWorkIndex.sol",
"contract": "AgentWorkIndex",
"compiler": {
"solcVersion": "0.8.20+commit.a1b79de6",
"solcSettingsKeccak256": "0xb68de617889310001775008072534de8400024510d5e09eaad212e2b3f942376"
},
"creationCodeBytes": 401,
"creationCodeKeccak256": "0xd8e698ee533ee2e293601190990c3762df48b66e8f8de0d7a7f4ef7ac797a781",
"runtimeCodeBytes": 374,
"runtimeCodeKeccak256": "0x2e8a42e082d6ee00eb3edfe933feef37e2eb9a81745c440c51e2af6c9b4ad5e1",
"create2": {
"deployer": "0x4e59b44847b379578588920ca78fbf26c0b4956c",
"saltPreimage": "FAO_AGENT_WORK_INDEX_V1",
"salt": "0x42e43b332008715cd34a0232b58cd6b4efa707de7fd9c87bac4fab22f11285d4",
"predictedAddress": "0x07db8c5bd5a2ea12de1d5a322b73cea536afff49"
},
"economicBytecodeIncluded": false
}
1 change: 1 addition & 0 deletions metadata/agent-work-p1-evidence.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions metadata/agent-work-p1-evidence.json.sha256
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
0x45d0ee726f5ac83cb03cf71090864fd78452a1e6e734b3e838f5f9f5d39a3649
25 changes: 25 additions & 0 deletions src/AgentWorkIndex.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

/// @notice Ownerless publication log for agent-work documents.
/// @dev It has no storage, authority, or custody; FAO economics never read it.
contract AgentWorkIndex {
error EmptyDocument();

event Published(
bytes32 indexed kind,
bytes32 indexed parentDigest,
bytes32 indexed documentDigest,
address publisher,
bytes document
);

function publish(bytes32 kind, bytes32 parentDigest, bytes calldata document)
external
returns (bytes32 documentDigest)
{
if (document.length == 0) revert EmptyDocument();
documentDigest = keccak256(document);
emit Published(kind, parentDigest, documentDigest, msg.sender, document);
}
}
52 changes: 52 additions & 0 deletions test/AgentDocumentGolden.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {Test} from "forge-std/Test.sol";

import {FAOTreasuryActions} from "../src/FAOTreasuryActions.sol";

contract AgentDocumentGoldenTest is Test {
function testPaymentEnvelopeMatchesPythonAndJavascriptGolden() public pure {
bytes memory document =
hex"7b22616d6f756e74223a22313135373932303839323337333136313935343233353730393835303038363837393037383533323639393834363635363430353634303339343537353834303037393133313239363339393335222c226173736574223a22307863636363636363636363636363636363636363636363636363636363636363636363636363636363222c22636861696e4964223a223131313535313131222c226b696e64223a2266616f2e7061796d656e74222c2272656365697074223a22307862633135636630316130393134303863313661623834333431613261656264613533343032623861333162343534333139396334643262313736643437336435222c22726563697069656e74223a22307864646464646464646464646464646464646464646464646464646464646464646464646464646464222c2273616c74223a22307865666566656665666566656665666566656665666566656665666566656665666566656665666566656665666566656665666566656665666566656665666566222c227461736b223a22307832336166653436316238643336653963306631633035303365316563316139386164373933373734383332346363343037333137343037386133306532333335222c2276223a2231222c227661756c74223a22307861616161616161616161616161616161616161616161616161616161616161616161616161616161227d";
bytes32 envelopeDigest = 0x60fccea2c2617a6e1f35bb536e2a48b7384a8b6d91d3486075c1f77981eadc18;
assertEq(keccak256(document), envelopeDigest);

FAOTreasuryActions.TransferAction memory action = FAOTreasuryActions.TransferAction({
asset: address(uint160(0x00cccccccccccccccccccccccccccccccccccccccc)),
recipient: address(uint160(0x00dddddddddddddddddddddddddddddddddddddddd)),
amount: type(uint256).max,
salt: envelopeDigest
});
bytes memory payload = FAOTreasuryActions.transferEvaluationPayload(
11_155_111, address(uint160(0x00aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa)), action
);
assertEq(
payload,
hex"27e49851e3b79673e847d7c12acc52a3936006b8517243a42df902b3df4e902e0000000000000000000000000000000000000000000000000000000000aa36a7000000000000000000000000aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa000000000000000000000000cccccccccccccccccccccccccccccccccccccccc000000000000000000000000ddddddddddddddddddddddddddddddddddddddddffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff60fccea2c2617a6e1f35bb536e2a48b7384a8b6d91d3486075c1f77981eadc18"
);
bytes32 actionHash = FAOTreasuryActions.transferHash(
11_155_111, address(uint160(0x00aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa)), action
);
assertEq(actionHash, 0x24a4125ae332e790e5fa9cbdc5d243b63a026e7b96fc9e796fd6d87706f2a87e);
assertEq(
uint256(actionHash),
16_573_152_149_377_518_413_156_023_629_436_668_511_050_935_944_683_357_744_569_870_274_751_223_736_446
);
}

function testAgentKindPreimagesMatchGolden() public pure {
assertEq(
keccak256("FAO_AGENT_TASK_V1"),
0xa87c1f2bd1ee275d3f44c021b929709db51ad8a945c2c34a0857974e28595821
);
assertEq(
keccak256("FAO_AGENT_RECEIPT_V1"),
0xe2c91b1ce0f47a0ac033aec054b0ce8dd8a0ca22e4812f61776ed60835734da1
);
assertEq(
keccak256("FAO_AGENT_PAYMENT_V1"),
0x8161a6637134aa32b72dafb5032097de770b8555713c2415d800ea5af322c7bd
);
}
}
89 changes: 89 additions & 0 deletions test/AgentWorkIndex.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {Test} from "forge-std/Test.sol";

import {AgentWorkIndex} from "../src/AgentWorkIndex.sol";

contract AgentWorkToken {
mapping(address => uint256) public balanceOf;

function mint(address recipient, uint256 amount) external {
balanceOf[recipient] += amount;
}

function transfer(address recipient, uint256 amount) external returns (bool) {
balanceOf[msg.sender] -= amount;
balanceOf[recipient] += amount;
return true;
}
}

contract ForceAgentWorkEther {
constructor() payable {}

function destroy(address payable recipient) external {
selfdestruct(recipient);
}
}

contract AgentWorkIndexTest is Test {
event Published(
bytes32 indexed kind,
bytes32 indexed parentDigest,
bytes32 indexed documentDigest,
address publisher,
bytes document
);

AgentWorkIndex internal index;

function setUp() public {
index = new AgentWorkIndex();
}

function testRejectsEmptyDocument() public {
vm.expectRevert(AgentWorkIndex.EmptyDocument.selector);
index.publish(bytes32(0), bytes32(0), "");
}

function testFuzzPublishesExactDigestAndEvent(
bytes32 kind,
bytes32 parentDigest,
address publisher,
bytes calldata rawDocument
) public {
bytes memory document = rawDocument;
vm.assume(publisher != address(0));
vm.assume(document.length != 0 && document.length <= 4096);
bytes32 digest = keccak256(document);

vm.expectEmit(true, true, true, true, address(index));
emit Published(kind, parentDigest, digest, publisher, document);
vm.prank(publisher);
assertEq(index.publish(kind, parentDigest, document), digest);
}

function testPublicationDoesNotWriteStorage() public {
bytes32 slot = keccak256("arbitrary slot");
assertEq(vm.load(address(index), slot), bytes32(0));
index.publish(bytes32(0), bytes32(0), "document");
assertEq(vm.load(address(index), slot), bytes32(0));
}

function testForcedAssetsCreateNoWithdrawalSeam() public {
AgentWorkToken token = new AgentWorkToken();
token.mint(address(index), 12 ether);
ForceAgentWorkEther force = new ForceAgentWorkEther{value: 1 ether}();
force.destroy(payable(address(index)));

assertEq(token.balanceOf(address(index)), 12 ether);
assertEq(address(index).balance, 1 ether);

(bool success,) = address(index)
.call(abi.encodeCall(token.transfer, (address(this), token.balanceOf(address(index)))));
assertFalse(success);
assertEq(token.balanceOf(address(index)), 12 ether);
assertEq(address(index).balance, 1 ether);
}
}
Loading