Docs menu

08 ยท 6 min read

Security

YieldLinks holds other people's tokens, so this page says what can go wrong, what stops it, and which test proves it. The long form is docs/THREAT_MODEL.md in the repository.

The contracts have not been audited. Testnet only until they are.

01A link is bearer value#

The private key in the URL fragment is the only thing that proves you may claim. Whoever has the link can claim it to any address. Everything else on this page limits what someone without the link can do.

A leaked link cannot be made safe in this design. What limits the damage: short expiries, a sender who can cancel at any time, and a UI that tells people to share it like cash.

02Who can do what#

PartyCanCannot
SenderCreate and cancel their own linksTouch other links, or take back a claimed one
Link holderClaim once, before expiry, to any addressClaim twice, claim after expiry, touch other links
RelayerPay gas for claimMove funds, call anything but claim, or change the recipient
DeployerBind the source once; withdraw its HBAR reserveMove escrowed tokens, change the controller, upgrade anything
AnyoneTrigger a refund after expirySend that refund anywhere but the sender

03Threats and what stops them#

ThreatWhat stops itEvidence
Mempool front-running a claimThe signature covers (linkKey, recipient), so another recipient fails recoverytest_claim_frontRunnerCannotRedirectFunds
Replaying a spent claimStatus moves to Claimed and the shares are burnedtest_claim_cannotBeReplayed
Replaying against a reused keyLinks are never deleted; a key is single use, forevertest_createLink_keyCanNeverBeReused
Cross-chain or cross-contract replayThe EIP-712 domain binds chain id and verifyingContract_hashTypedDataV4 in claim
Wrong signerECDSA.recover must equal linkKeytest_claim_revertsOnWrongSigner
First-depositor inflationVirtual shares and assets (VIRTUAL = 1000)test_firstDepositorInflation_costsAttackerMoreThanVictimLoses
Insolvency from roundingMaths rounds against the user; payouts never exceed the pooltestFuzz_poolStaysSolvent
Claim after expiryclaim reverts after expiry; refund takes overtest_claim_revertsAfterExpiry
A stranger cancels earlyOnly the sender can refund before expirytest_refund_strangerCannotCancelBeforeExpiry
Source drained by an outsiderSource functions are onlyController, set oncetest_source_onlyControllerMovesFunds
Relayer drained with junk claimsSimulate first, call only claim, cap gas, 10 requests a minute per IPapp/api/claim/route.ts
Bulk account creationRequires an open link's signature over the new public key; one account per linked25519.test.ts
Link that did not prepayRefused below 1.35 HBAR before anything is spent (HTTP 402)PrepaidFeeTest
Fee payout revertsLow-level call; on failure the fee goes to pendingFeesNoReceiveSender, NoReceiveSubmitter
Recipient short of the gift10-unit rounding reserve, value capped at totalAssets, bounded shortfallRecipientReceivesAtLeastTheAmountTest
ReentrancynonReentrant on every entry point; state settled before payoutYieldLinks.sol

04Known gaps#

  • No audit. The fuzz and unit tests cover the contracts, not the relayer routes or the React code.
  • The pool can lose value. The source is only as safe as SaucerSwap's Infinity Pool.
  • SAUCE is volatile. A gift's dollar value moves with the market.
  • Generated accounts cost the relayer. About 0.58 HBAR each, and the one-account-per-link record is in memory, reset on restart.
  • The relayer is one process. Its rate limit is in memory and sends go through one nonce. Production needs a queue and shared limits.
  • In-browser wallets are demo-grade. A recipient who loses the key loses the gift. Use passkeys or an embedded wallet in production.
  • Links live in localStorage. The sender's browser keeps full URLs, secrets included, so they can re-share them.

Found something? Open an issue for anything non-sensitive. For a vulnerability, contact the repository owner privately first.