ShijimaDocs
Open app
Architecture

The agent contract

One small contract per owner, cloned from a verified implementation. No upgrade path, no admin. Its guards hold even if the operator key is stolen.

Reviewed 2026-09-26

Every agent is a Desk: a minimal clone (EIP-1167) of one implementation, created by DeskFactory. Solidity 0.8.28, OpenZeppelin 5. There is no upgrade path and no admin. The owner is fixed at creation and never changes.

Creating one

createDesk(config, salt) clones the implementation and sets owner = the caller, always. So nobody can create an agent that belongs to someone else. predictDesk(owner, salt) gives the address before it exists. The website fills the config: Shijima's operator, your two limits, and all 16 approved Stock Tokens with their pinned pools and price feeds.

Who can call what

FunctionOwnerSession keyOperator (the agent)
buyyesnoyes, guarded
sellyesyes, guardedyes, guarded
sweepToVault · redeemFromVaultyesnoyes
checkpointyesnoyes, even while paused
pauseyesyesyes
unpauseyesnono
withdraw (to the owner only)yesyesno
batch (several calls, one signature)yesyesno
setLimitsyeslower onlyno
setOperator · allowToken · disallowToken · grantSessionyesnono
revokeOperatoryesyesno

The session key is optional: a key your browser holds for up to 7 days, so small safe actions like pausing or lowering a limit do not need your wallet each time.

The guards on the operator

Every operator trade must pass all of these, or the whole transaction reverts:

  • Per-trade and per-day limits. The day is a fixed window that restarts 24 hours after it began, at the next trade. A sale counts at the larger of the USDG it received and the feed's value of what it sold.
  • An 8% price floor, computed in the contract from the Chainlink feed (BAND_BPS = 800). A buy must get at least 92% of the tokens the feed price says; a sale must get at least 92% of the feed value.
  • A healthy feed: a positive price, no older than 6 days, and the token not reporting its price as paused.
  • The pinned pool. The owner pins each token's Uniswap fee tier. The operator never chooses a pool, so it cannot route through an empty or seeded one.
  • Allowed tokens only for buys, up to 16. A token the owner disallows can still be sold.
  • A deadline and a non-zero decision hash on every call.
  • Not paused. Only checkpoint and pause work while paused.
  • Output to the desk. Every swap's recipient is the contract itself. Approvals are set for the exact amount and reset to zero after.

The contract cannot receive ETH. Trades, sweeps, redeems and withdrawals are protected against re-entry.

The on-chain record

buy, sell, sweepToVault, redeemFromVault and checkpoint each take a decisionHash and extend the contract's own chain:

seq  = seq + 1
head = keccak256(abi.encode(head, seq, decisionHash))

Each emits an event carrying the hash: Bought, Sold (with countedUsdg), Swept, Redeemed or Checkpoint. The TypeScript side computes the same head, so the contract and the database can be compared at any time.

Deployments

v1 implementation (verified on Blockscout)
0x90ff69C78014d06e3f09DC0985E83Cd8338aFe0F

Proven on mainnet

The limits were tested with the real operator key, on purpose, and the chain refused:

The first real trade, decided by SERV and sealed with its fingerprint: 0xba77…5cdf.

Not audited

The contract is small and has no upgrade path or admin, but it has not been audited. Amounts are small.

Source notes

This page was checked against the code on 2026-09-26. These are the files it follows.

On this page