The router
ChameleonRouter is one contract of about 100 lines. It holds no funds between calls, has no owner, charges no fee and cannot be upgraded.
Constructor
constructor(address announcer_)
The announcer is the ERC-5564 singleton, 0x55649E01B5Df198D18D95b5cc5051630cfD45564 on Robinhood Chain. It is immutable.
buy
function buy(
address curve,
uint256 minTokensOut,
address stealth,
bytes calldata ephemeralPubKey,
bytes1 viewTag,
uint256 gasDust
) external payable returns (uint256 tokensOut)
msg.value is the amount spent on the curve plus gasDust.
| Parameter | Meaning |
|---|---|
curve | A Pons curve. Take it from the factory's TokenLaunched event. |
minTokensOut | The least that must land on the stealth address, or the call reverts. |
stealth | The one-time address derived from the receiver's meta-address. |
ephemeralPubKey | 33-byte compressed public key of the sender's one-time key. |
viewTag | First byte of the hashed shared secret. |
gasDust | ETH forwarded to stealth so it can pay for its own transactions. Must be below msg.value. |
What it does, in order
- Locks against re-entry, checks the inputs.
- Reads the curve's token with
curve.token()and the stealth address's balance. - Calls
curve.buy{value: quoteIn}(quoteIn, minTokensOut, stealth). - Measures the balance change. Reverts if it is zero or below
minTokensOut. - Sends
gasDusttostealth. - Calls
announcer.announce(1, stealth, ephemeralPubKey, metadata). - Returns any ETH the curve handed back to the caller.
Errors
| Error | Cause |
|---|---|
Reentrancy() | A nested call into buy. |
BadStealth() | stealth is the zero address. |
BadEphemeralKey() | The key is not 33 bytes. |
DustTooLarge() | gasDust is not below msg.value. |
NothingToBuy() | The stealth address received no tokens. |
BelowMinimum(got, wanted) | Slippage: fewer tokens than minTokensOut. |
DustTransferFailed() | The stealth address rejected the gas. |
RefundFailed() | The caller could not receive a refund. |
Invariants
- The router's ETH and token balances are zero after every call.
- The
buyerthe curve records is always the router. - No function can be called by an owner, because there is no owner.
Tested against the real chain
The 11 tests in contracts/test/ChameleonRouter.t.sol run on a fork of Robinhood Chain against the real Pons curve and the real announcer:
| Test | What it proves |
|---|---|
test_tokensLandOnStealthNotOnCaller | The tokens arrive on the stealth address, none on the caller or router. |
test_gasDustReachesStealthAndRouterKeepsNothing | The dust arrives and the router ends at zero. |
test_announcementMatchesErc5564Layout | Scheme 1, correct indexed fields, 57-byte metadata with view tag, selector, token and amount. |
test_curveSeesTheRouterAsBuyerNeverTheUser | CurveBuy.buyer is the router. |
test_stealthCanSellOnItsOwnUsingTheDust | The stealth address approves and sells with only the dust it received. |
test_revertsBelowMinimum | Slippage protection works. |
test_revertsOnBadEphemeralKey | Input checks. |
test_revertsOnZeroStealth | Input checks. |
test_revertsWhenDustSwallowsEverything | Input checks. |
test_manyBuysManyStealthAddressesRouterStaysEmpty | Five buys to five addresses leave the router at zero. |
test_gasUsedForOneBuy | About 188,000 gas on the fork. |