@gluwa/asc-contracts
v0.2.1
Published
Solidity contracts and libraries for the ASC (Attestcoin Smart Contracts) ecosystem on the Gluwa Creditcoin Network. Distributed as source for consumption by Foundry/forge.
Downloads
159
Keywords
Readme
ASC Contracts
This repository is responsible for housing the core smart contracts necessary to support Attestcoin Smart Contracts (ASC) on the Gluwa Creditcoin Network.
For a deeper explanation of how the contracts below relate to each other — including sequence
diagrams for the publish → attest → deliver → acknowledge flow and the relayer-assisted fee-payment
flow — see docs/ARCHITECTURE.md.
Repository layout
contracts/
├── common/ # shared libraries used by write-ability and readability (e.g. EvmV1Decoder)
├── mocks/ # test-only helpers; excluded from the published npm package
├── readability/ # ASCBase and other readability ASC primitives
└── write-ability/
├── *.sol # core messaging + fee/relaying contracts (see below)
├── deployer/ # OutboxDeployer (versioned factory registry/access control) +
│ # OutboxFactory (CREATE2 deployer for Outbox)
├── abstract/ # interfaces implemented by the contracts above
├── common/ # shared libraries: storage layout, decoding, proof verification, oracle
│ # math, the relayer fee ledger, token-bridge helper types
└── error/ # custom-error libraries shared across contractsReadability base
ASCBase(contracts/readability/ASCBase.sol) — abstract base for readability ASCs. Verifies foreign-chain transaction proofs via the native block-prover precompile (0xFD2), dedupes by query id, then delegates to_processAndEmitEvent. Bridge and loan examples inherit this.
Shared decoding libraries
EvmV1Decoder(contracts/common/EvmV1Decoder.sol) - decodes the tx/receipt data of an EVM transaction (fields, logs, log filtering by event signature). Shared by write-ability contracts (acknowledgment / delivery decoding) and readability ASCs that parse proven transactions.
Readability-adjacent decoding libraries
ASCSdkV1TxBytesLib(contracts/write-ability/common/ASCSdkV1TxBytesLib.sol) - decodes the prover's chunked(txType, bytes[] chunks)transaction encoding into a flat, typed struct, handling legacy and EIP-1559-style transactions.
Writability Contracts
The write-ability layer lets a dApp on Creditcoin publish a message that is attested by the
validator/attestor set, relayed to a destination chain, and (optionally) acknowledged back on
Creditcoin via a trust-minimized native proof. It splits into a core messaging path and an optional
fee/relaying layer built on top of it — see the class and sequence diagrams in
docs/ARCHITECTURE.md for how the two interact.
Core messaging
Outbox(Outbox.sol) /OutboxFactory(deployer/OutboxFactory.sol) /OutboxDeployer(deployer/OutboxDeployer.sol) - source-side message publishing.OutboxDeployerregisters and enables versionedOutboxFactoryimplementations and is the intended (owner-gated) caller ofdeployOutbox; the factory's owndeployOutboxis permissionless but CREATE2-salts onmsg.sender, so a stray direct call can't collide with a deployment made through the deployer. EachOutboxgets its own owner/validator/rate-limit/AttestorVault/FeeRegistry/ATTEST-token configuration.publishMessage(canAck, payload)derives a per-emitter sequencedmessageIdand pullscoreFeefrom the configuredFeeRegistry;publishMessageFromlets a registered trusted forwarder (e.g.RelayerContract/RelayerContractLite) publish on a dApp's behalf, but only for emitters that separately opted in viaapproveForwarder;routeCoreFee/routeAckFeelet a trusted forwarder deposit the core fee and an acknowledgment-fee bounty after the fact (the latter upgrading a message published withcanAck = false);acknowledgeMessage/batchAcknowledgeMessagesare gated on the configuredvalidator.Inbox(Inbox.sol, formerlySimpleInbox.sol) - destination-side delivery contract. Delivers to a single fixedmessageDispatcher(anIMessageReceiver, typically built on theMessageReceiverBaseabstract helper) configured at construction, rather than a destination decoded from the payload. Delegates vote checking to a pluggableIVoteValidator, supports pending/retry delivery, isOwnable2Step+Pausable(owner canpause()/unpause()message delivery), and reverts with custom errors (error/InboxErrors.sol) instead of strings.EOAValidator(EOAValidator.sol) - the productionIVoteValidator: ECDSA recover against an attestor set with a configurable quorum (numerator/denominator + addition, commonly set up as a2N/3 + 1threshold), EIP-2 malleability hardening, and replay-protected, permissionless attestor-set updates signed by the current attestor set. Attestor membership itself now lives in the sharedAttestorRegistryrather than a local mapping on this contract.AttestorRegistry(AttestorRegistry.sol) - standalone, owner-managed attestor set shared by every consumer that needs attestor membership (EOAValidator,AttestorVault.settle), so the set is maintained and audited in one place instead of duplicated per-consumer. An owner-authorized updater (typicallyEOAValidator, for its attestor-votedsubmitAttestorSetUpdate) may also add/remove/replace attestors.AcknowledgmentValidator(AcknowledgementValidator.sol) - proof-based acknowledgment, run on the source chain. It verifies a proof via the sharedASCProofVerifier(the same verifierRelayerContract.claimDeliveryuses) that aMessageDeliveredevent was emitted by a trusted destinationInbox, decodes the log withEvmV1Decoder, and callsOutbox.acknowledgeMessage. Submission is permissionless - the proof is self-validating. It also custodies each message's user-set acknowledgment-fee bounty (deposited byOutbox.routeAckFee) and pays it to whoever submits the winning proof, with a 7-day payer refund path (refundAckFee) if no proof arrives.
Fee & relaying layer
RelayerContract(RelayerContract.sol) /RelayerContractLite(RelayerContractLite.sol) -Outboxtrusted forwarders that let a relayer front the publish call for a dApp. Both validate an off-chain-signedRelayerTypes.Quote(common/RelayerTypes.sol), pullcoreFee + relayPrice + acknowledgmentPrice(+ tip, full variant only) from the payer (ERC-20 transfer, EIP-3009 authorization, or native coin forpayInNativequotes), forward the core/ack fees throughOutbox.routeCoreFee/routeAckFee, and deposit the relay reward (+ tip) into the activeRelayerFeeVault. A quote'sacknowledgmentPrice > 0is the acknowledgment request - there is no separatecanAck/requiresAckargument any more.RelayerContractLitedrops tips and on-chain quote-floor checking (noASCRelayingQuoterdependency; quotes are checked against an owner-managed whitelist of off-chain Quoter EOAs instead) for a cheaper, minimal deployment. Both mix inRelayerFeeLedger(common/RelayerFeeLedger.sol) for their per-message fee bookkeeping and activate a vault post-construction via owner-onlysetRelayerFeeVault(the vault's immutablerelayerContract()must already point back at the caller).AttestorVault(AttestorVault.sol) - holds attestation (coreFee) payments per message and settles them to the attestor set once attestation is confirmed by the configured validation contract, burning a configurable share (capped at 20%).settlenow also takes the list of settling attestors and rejects any payee the configuredAttestorRegistry(or compatibleisAttestorsource) does not recognize. Unsettled deposits are refundable to their payer after a configurable delay (7 days by default).RelayerFeeVault(RelayerFeeVault.sol) - pure fee custody, drastically simplified: it holds ATTEST or native coin and pays out only when instructed by its boundRelayerContract/RelayerContractLiteviapay(to, amount, native). It keeps no per-message ledger of its own - routes, amounts, settlement flags, and gas-limit/tip bookkeeping all moved to the relayer contracts'RelayerFeeLedgermixin; the vault only executes payouts (with a push-then-pull fallback viapendingNativeWithdrawals/withdrawNativeif a native push fails).FeeRegistry(FeeRegistry.sol) /ICoreFeeProvider(abstract/ICoreFeeProvider.sol) - the registryOutbox.coreFee()/publishMessagenow readcoreFeefrom, replacing the previous direct dependency onASCRelayingQuoter.FeeRegistryis a thin, swappable (Outbox.setFeeRegistry) wrapper around a pluggableICoreFeeProvider.get_core_fee(chainKey)- in production the Creditcoin core-fee precompile;contracts/mocks/MockCoreFeeProvider.solis the settable test stand-in.ASCRelayingQuoter(ASCRelayingQuoter.sol) - live relay-fee quoting in either TWAP or Uniswap-v3-pool pricing mode, an acknowledgment-fee floor (getAcknowledgmentFee, refreshed alongside prices) thatRelayerContract/RelayerContractLiteenforce against a quote'sacknowledgmentPrice, and the authorized-quoter allowlist the fullRelayerContractchecks signed quotes against. Its owncoreFeeInAttest/getCoreFee/setCoreFeeare no longer read byOutbox(that now comes fromFeeRegistry) - treat that pair as a legacy/unused view, not part of the live pricing path.TWAPReader(TWAPReader.sol) - an on-chain, oracle-fed cumulative-price TWAP for the ATTEST/CTC exchange rate, consumed byASCRelayingQuoter.
Proof & decoding libraries
ASCProofVerifier(common/ASCProofVerifier.sol) - the shared entry point for verifying a binary-Merkle inclusion + continuity proof via the native block-prover precompile, returning the proven raw transaction bytes. Used by bothAcknowledgmentValidatorand theRelayerContract/RelayerContractLiteclaimDeliverypath.EVMDeliveryDecoder(common/EVMDeliveryDecoder.sol) - decodes a proven delivery transaction against a per-destination-chain-ID trustedInboxaddress, and reports whether it emittedMessageDelivered(success) orMessagePending(reverted/out-of-gas) for the givenmessageId.QueryProofVerificationLib/BlockProverTypes(common/) - shared Merkle/continuity proof types and helpers used byASCProofVerifier.
Token bridge
ASCBridgeLiquidityOperator(ASCBridgeLiquidityOperator.sol) - Creditcoin-hub-side bridge operator. Publishes outbound bridge intents through a configuredOutboxtrusted forwarder (RelayerContract), escrows or burns source tokens, and coordinates inbound mint/release via proved settlement transactions. Payload encoding usesBridgeMessageCodecV1/ASCBridgeTypes.BridgeMessage.ASCBridgeMintDestination(ASCBridgeMintDestination.sol) - durable mint executor for the inbound bridge path. The hub operator callsexecuteMintafter validating a proved source-chain settlement transaction viaBridgeIntentDecoder. This is not anIMessageReceiverand must not be wired as anInboxmessageDispatcherfor outboundbridgeTopayloads.BridgeIntentDecoder(common/BridgeIntentDecoder.sol) - decodes proved source-chain settlement transactions into inbound bridge intents.BridgeMessageCodecV1(common/BridgeMessageCodecV1.sol) - canonical ABI codec for V1 bridge payloads.
Interfaces, errors & helpers
- Interfaces (
abstract/) - the pluggable seams implemented above:IOutbox,IOutboxFactory,IVoteValidator,IAttestorVault,IAttestorRegistry,IFeeRegistry,ICoreFeeProvider,IRelayerContract,IRelayerContractLite,IRelayerFeeVault,IASCRelayingQuoter,ITWAPReader,IASCProofVerifier,IDeliveryDecoder,IInbox,IMessageReceiver(+MessageReceiverBase, the abstract base a dApp inherits from to receive ASC messages),IERC3009,IERC20MintBurn,IPenguinSwapV3Pool,IASCBridgeLiquidityOperator,IASCBridgeTokenDestination,IBridgeIntentDecoder. - Custom-error libraries (
error/) -CommonErrors,OutboxErrors,RelayerErrors,InboxErrors,ASCBridgeLiquidityOperatorErrors. common/CanonicalTokenCall.sol,TokenAmountNormalization.sol,TokenExecutionCommitment.sol,ASCBridgeTypes.sol- helper libraries and shared bridge message structs consumed byASCBridgeLiquidityOperatorandBridgeMessageCodecV1.
Note: two distinct
INativeQueryVerifierinterfaces exist at different paths (write-ability/INativeQueryVerifier.solandwrite-ability/common/INativeQueryVerifier.sol) — they are not interchangeable, and only thecommon/copy is currently wired into any contract (viaASCProofVerifier). See the "Known naming quirks" section ofdocs/ARCHITECTURE.mdbefore assuming they're the same type, or that the top-level one is still in use.
Building and Running the Smart Contract Tests
The contracts are a Hardhat project. Tests are written in TypeScript (Mocha/Chai + ethers v6) and
live in test/; test-only helper contracts (mocks/stand-ins) live in contracts/mocks/ and are not
part of the published npm package.
Prerequisites: Node.js 20+. Install dependencies once with:
npm installCompile (build) the contracts. Hardhat downloads the pinned solc 0.8.28 on first run:
npx hardhat compileRun the full test suite:
npm test
# or: npx hardhat testRun the tests with a Solidity coverage report (written to ./coverage/ and ./coverage.json). This
is what CI runs:
npm run coverage
# or: npx hardhat coverageCI then enforces a coverage floor (currently 70% statements / 50% branches / 70% functions / 65%
lines, set a little below the actual numbers so incidental fluctuation doesn't fail a PR) against
./coverage.json via Istanbul's check-coverage, which ships as a
transitive dependency of solidity-coverage - no separate install needed. Run it locally after
npm run coverage:
npm run coverage:checkRaise the thresholds in the coverage:check script in package.json as real coverage improves;
don't lower them to make a PR pass.
Hardhat deployment scripts
Scripts live under scripts/hardhat/. See .env.example for variables.
| Script | Deploys | Use when |
|--------|---------|----------|
| deployWriteAbility.ts | Outbox, Relayer, vaults, quoter, optional Inbox | Bootstrap the messaging stack only. Does not deploy token-bridge operators. |
| deployBridge.ts | ASCBridgeLiquidityOperator, ASCBridgeMintDestination, BridgeIntentDecoder | Token bridge on top of an existing hub (OUTBOX, RELAYER, PROOF_VERIFIER). |
| bridgeTo.ts | (smoke test) | Publish one bridgeTo transaction; fails closed on stale TWAP, zero coreFee, or missing forwarder approval. |
Fresh devnet token bridge (hub publish + inbound mint wiring):
deployWriteAbility.tswithDEPLOY_FRESH_HUB=true(orDEPLOY_FULL_STACK=true, same hub path)deployBridge.ts— hubASCBridgeLiquidityOperator+ASCBridgeMintDestination(inbound mint)
End-to-end outbound delivery (hub bridgeTo → destination Inbox) additionally requires a
destination IMessageReceiver that decodes BridgeMessageCodecV1 payloads and mints/releases
tokens. That receiver is not ASCBridgeMintDestination (which only exposes executeMint for
the inbound bridgeFromIntent path). Deploy it separately, then:
deployWriteAbility.tswithDEPLOY_INBOX=trueandMESSAGE_DISPATCHER=<destination IMessageReceiver>deployBridge.tswithREMOTE_CLIENT_OPERATOR=<same destination IMessageReceiver>to pair the hub route (optional metadata on the hub operator)
npm run deploy:write-ability
npx hardhat run scripts/hardhat/deployBridge.ts --network usc_devnetContinuous Integration
Every pull request runs:
hardhat(.github/workflows/hardhat.yml) - compiles the contracts, runsnpx hardhat coverage, fails the build if coverage drops below the floor innpm run coverage:check(see above), and uploads the coverage report as a build artifact regardless of that outcome.solhint(.github/workflows/solhint.yml) - lintscontracts/**/*.solwith Solhint (npm run lint:solidity); the ruleset lives in.solhint.json.slither(.github/workflows/slither.yml) - runs Slither static analysis overcontracts/write-abilityand uploads findings to GitHub code scanning as SARIF (informational — it does not fail the build).
Published packages
Pushing a tag of the form vX.Y.Z publishes two npm packages (see "Releasing New Contract
Versions" below):
@gluwa/asc-contracts- the Solidity source itself (contracts/write-ability/**/*.sol,contracts/common/**/*.sol, andcontracts/readability/**/*.sol), for consumption by Foundry/forge or Hardhat.@gluwa/asc-contracts-abi- just the compiled ABI JSON for the same contracts, for consumers (relayers, indexers, frontends) that only need to encode/decode calls or parse events without pulling in Solidity source. Built by.github/workflows/abi-publish.yml. To generate it locally:npm run build:abi-packageThis compiles the contracts and writes the package to
dist/abi-package/(gitignored). Each contract's ABI is available as a named export (e.g.require('@gluwa/asc-contracts-abi').Outbox) and as an individualabi/<ContractName>.jsonfile.
Releasing New Contract Versions
- Bump the
versionfield inpackage.jsonto match the tag you're about to push. - Push a new tag of the format
vX.Y.Z. This triggers thenpm-publishworkflow (publishes@gluwa/asc-contractsand creates a GitHub release) and theabi-publishworkflow (publishes@gluwa/asc-contracts-abi) in parallel. Both compare the tag againstpackage.json's version and fail if they don't match. - Update the contract version used in the
attestcoin-protocol-examplesandCreditcoin3repositories. This usually just involves updating the imported versions inpackage.jsons. - If there's a major interface change, consider re-verifying the contracts on services such as Blockscout.
