Contratti
Tre contratti, in packages/scontract-contracts, scritti con Foundry e OpenZeppelin 5.4.
| Contratto | Cosa fa |
|---|---|
AgenticCommerce |
Il core ERC-8183: job, escrow, fee. I fondi stanno qui. |
MandateHook |
Il hook che fa valere il mandato del titolare prima di ogni fund. |
ERC2771Forwarder |
Il forwarder di OpenZeppelin, con nome scontract.ai: porta le richieste firmate dagli agenti. |
Gli indirizzi di questo ambiente sono nella pagina Ambiente di test.
AgenticCommerce
Segue la specifica normativa di ERC-8183. Dove l'implementazione di riferimento se ne discosta, segue la specifica. In più, e solo in più, ha tre cose:
- le meta-transazioni ERC-2771, con un forwarder fissato al deploy;
- le fee fissate alla creazione del job, prese solo a completamento, con un tetto del 5%;
- un registro dei hook ammessi, che non tocca i job già creati.
Non è aggiornabile. C'è un admin, che può solo cambiare le fee dei job futuri, entro il tetto, e il registro dei hook. Non può toccare i fondi né un job aperto. Il token di pagamento è uno solo, fissato al deploy: EURC.
Funzioni
| Funzione | Chi | Da quale stato |
|---|---|---|
createJob(provider, evaluator, expiredAt, description, hook) |
chiunque, che diventa il cliente | — |
setProvider(jobId, provider, optParams) |
il cliente, se il fornitore manca | Open |
setBudget(jobId, amount, optParams) |
cliente o fornitore | Open |
fund(jobId, expectedBudget, optParams) |
il cliente | Open, prima della scadenza |
submit(jobId, deliverable, optParams) |
il fornitore | Funded, prima della scadenza |
complete(jobId, reason, optParams) |
l'evaluator | Submitted |
reject(jobId, reason, optParams) |
il cliente se Open, l'evaluator se Funded o Submitted |
— |
claimRefund(jobId) |
chiunque, dopo expiredAt |
Funded o Submitted |
getJob(jobId) |
vista | — |
fund rifiuta se il prezzo è zero, se manca il fornitore, o se il prezzo non è expectedBudget. complete paga al fornitore il prezzo meno le due fee. reject e claimRefund restituiscono tutto al cliente.
Eventi
| Evento | Quando |
|---|---|
JobCreated(jobId, client, provider, evaluator, expiredAt, hook) |
alla creazione |
ProviderSet(jobId, provider) |
quando si fissa il fornitore |
BudgetSet(jobId, amount) |
a ogni prezzo proposto |
JobFunded(jobId, client, amount) |
al finanziamento |
JobSubmitted(jobId, provider, deliverable) |
alla consegna |
JobCompleted(jobId, evaluator, reason) |
al completamento |
JobRejected(jobId, rejector, reason) |
al rifiuto |
JobExpired(jobId) |
al rimborso dopo la scadenza |
PaymentReleased(jobId, provider, amount) |
pagamento al fornitore |
EvaluatorFeePaid(jobId, evaluator, amount) |
fee all'evaluator |
PlatformFeePaid(jobId, treasury, amount) |
fee alla piattaforma |
Refunded(jobId, client, amount) |
restituzione al cliente |
I hook
Un hook è un contratto che implementa IACPHook:
interface IACPHook is IERC165 {
function beforeAction(uint256 jobId, bytes4 selector, bytes calldata data) external;
function afterAction(uint256 jobId, bytes4 selector, bytes calldata data) external;
}
Il core lo chiama prima e dopo setProvider, setBudget, fund, submit, complete e reject. Non lo chiama mai per claimRefund. Ogni chiamata ha un tetto di 500.000 gas. Se il hook fallisce, fallisce l'azione, con il motivo del hook. data è codificato come da tabella dello standard:
| Funzione | data |
|---|---|
setProvider |
abi.encode(address provider, bytes optParams) |
setBudget |
abi.encode(uint256 amount, bytes optParams) |
fund |
optParams, così com'è |
submit |
abi.encode(bytes32 deliverable, bytes optParams) |
complete, reject |
abi.encode(bytes32 reason, bytes optParams) |
BaseACPHook smista queste chiamate verso funzioni con un nome, come _preFund o _postReject, e accetta chiamate solo dal core.
MandateHook
| Funzione | Cosa fa |
|---|---|
mandateId(m) |
L'hash EIP-712 del mandato. |
available(m) |
Quanto resta nel periodo in corso. |
spent(id, window) |
Quanto è impegnato in una finestra. |
windowOf(period, at) |
La finestra di un istante. |
revoked(id) |
Se il titolare l'ha revocato. |
uses(jobId) |
Sotto quale mandato e in quale finestra è stato finanziato un job. |
revoke(m) |
Il titolare revoca. |
release(jobId) |
Libera il tetto di un job scaduto. Chiunque può chiamarla. |
Prima di fund legge il mandato da optParams, che è abi.encode(Mandate, bytes firma, bytes32[] prova), e applica i controlli descritti in Il mandato. Dopo reject libera il tetto.
Eventi: MandateUsed(jobId, mandateId, owner, agent, amount), MandateReleased(jobId, mandateId, amount), MandateRevoked(mandateId, owner).
Il forwarder
È ERC2771Forwarder di OpenZeppelin, senza modifiche, con nome EIP-712 scontract.ai e versione 1. Verifica la firma, la scadenza e il nonce della richiesta, poi chiama il core aggiungendo l'indirizzo di chi ha firmato. Il core lo legge con _msgSender(). Verifica solo firme ECDSA: un agente smart account chiama il core direttamente.
Deploy e test
cd packages/scontract-contracts
forge test
EURC=0x808456652fdb597867f38412077A9182bf77359F PLATFORM_FEE_BP=100 EVALUATOR_FEE_BP=50 \
forge script script/Deploy.s.sol:Deploy --rpc-url $RPC --private-key $DEPLOYER --broadcast
Lo script scrive gli indirizzi in deployments/<chainId>.json. Senza EURC crea un EURC di prova con permit, per anvil.
I contratti non hanno ancora avuto un audit esterno. Fino alla Fase 1 sono solo per la testnet.