From 0cea9c553cb938721f126f82d491b457c09549fc Mon Sep 17 00:00:00 2001 From: SupremaLex Date: Tue, 18 Aug 2026 18:48:22 +0300 Subject: [PATCH] Hold value against a platform handle nobody has claimed yet The app must let somebody send to `@alice` before `@alice` exists on chain. The Bank cannot: its escrow is keyed by an account's immutable id, and a sender who knows only a handle has no id to key on. So `HandleEscrow` keys by the handle. A NEW contract, not a change to the Bank diamond. Two state-changing functions and one mapping do not warrant `LibDiamond`, a cut/loupe/ownership facet set, a `BankInit` and hand-rolled modifiers, and the point of this contract is that it is not part of the transfer stack. It takes the `IdentityNames` shape instead: UUPS behind `ERC1967Proxy`, `Ownable2Step`, the transient reentrancy guard. **The escrow is for the window before a handle is claimed, and only that.** A deposit for a handle that already resolves is paid STRAIGHT THROUGH to its holder -- once an account has claimed its identity, holding the value would add a claim transaction and reach the same wallet. `Forwarded` and `Deposited` are separate events because an indexer must tell "this is waiting" from "this was delivered", and no balance changed for it to read. What `Forwarded` reports is what the holder GAINED, measured across the transfer, so a fee-on-transfer token cannot leave the only record of a payment overstating it. That makes a deposit depend on the recipient: a holder that cannot receive value fails the call. It is the honest outcome -- the sender learns, instead of the value waiting in a slot only that same wallet could ever claim. The race is accepted: one calldata has two outcomes depending on whether it lands before or after a `bind` in the same block. Both deliver to the holder of the handle, so the difference is one transaction, not one of destination. **`claim` checks the destination, not only the caller.** The zero address ACCEPTS a native transfer, so an unset recipient would empty the slot, burn the value and log a success; this contract's own address would empty the books and leave the value here as surplus nothing points at. Neither is recoverable -- there is no refund and no owner lever -- so both are refused. **The handle is the whole key. There is no deadline and no refund.** That was decided deliberately and the consequences are written down rather than left to be discovered, each pinned by a test named as intent: * a platform that recycles a handle hands the new holder whatever accumulated for the previous one; * after a rename the slot waits for whoever proves the freed handle next, which may be a different account; * neither the depositor nor the owner can take a deposit back. **What the escrow keys on is NOT what it validates against.** The slot comes from a rules-independent form of the text -- trim spaces, drop one leading at-sign, fold A-Z -- so the owner reconfiguring a platform's normalization cannot move money already held. The text is separately checked against the platform's CURRENT rules on the way in, so nothing funds a slot no proof could ever claim. Immunity from re-keying is not immunity from stranding, and the header says so: a claim is authorized by `resolveHandle`, which normalizes under the rules of the moment, so an owner who narrows a platform makes the affected handles resolve to nobody and their value waits, at the same key, until compatible rules return. A test narrows X after a deposit and pins both halves. The at-sign is DROPPED rather than refused, exactly as `stripLeadingAt` does. That is safe for every expressible `Rules` -- a non-email platform refuses `0x40` outright and an email needs a name part before its `@` -- so no platform can accept a handle beginning with one, and both spellings reach a single slot instead of splitting one identity's money across two keys. The invariant that keeps this true is recorded where the folding happens. The validation needs the rules, so `IdentityNames` gains `rulesOf`. Without it the escrow either accepts text that can never resolve -- and there is no refund -- or keeps its own copy of the rules and disagrees with the naming system the first time `setPlatform` runs. `resolveHandle` cannot answer the question: it returns the zero address both for a handle nobody holds and for text nobody could hold, and those two must not be confused when money is about to move. The transform, unlike the rules, is PINNED here at compile time. That is what makes the key independent of configuration, and it means the naming system cannot change its normalizer alone: such an upgrade has to carry an upgrade here, and a `Rules` field appended there would be dropped by this decoder. Said in the header rather than left to be discovered. The lemma is checked across all 44 rows of `HandleVectors`, with no row skipped, and the slot derivation is pinned by a literal computed from the formula rather than read back out of the contract -- Python and Rust both reproduce it. Claim is authorized by `resolveHandle(platformId, handle) == msg.sender` and nothing else, drains one token's slot, and pays a recipient the claimer names. Copying the calldata gains nothing, because the check is against the caller. The rules are not re-checked on the way out: text that no longer normalizes resolves to nobody, so the holder check already refuses it, and re-checking would only replace an honest `NotTheHolder` with a misleading `UnusableHandle`. **The naming system's owner is part of this contract's trust base**, and the header now says so instead of claiming the owner cannot reach a balance. Authorization is `resolveHandle` and nothing else, so whoever decides what that answers can take what is held: `setVerifier` installs a verifier, a `bind` through it makes the owner the holder of any handle, and `claim` pays it. Two ordinary transactions, no upgrade and no proxy event. It adds no new party -- the same key already decides which proofs mint names at all -- but pretending it does not exist would be worse than naming it. Fee-on-transfer safety: an escrowed deposit credits the balance the contract actually gained, so the books never promise more than it holds. No pause. A pause on `claim` freezes other people's money behind an owner key; the emergency lever is the upgrade, which is already visible. Every guard is proven red without its code: deposit not checking the text against the platform's rules, the at-sign kept instead of folded, the amount asked for credited instead of the balance gained, no reentrancy guard on deposit, claim paying before it zeroes the slot, claim not checking the caller holds the handle, claim not checking the destination, a token deposit carrying ether, native value not equal to the amount, a held handle escrowing instead of paying through, the ERC-20 payout going nowhere, `Forwarded` reporting the amount asked for, and the storage root reordered under an upgrade. The reentrancy tests reenter with calls that would SUCCEED -- a token that deposits again from inside its own transfer, and a claimer that claims again from inside its payout while somebody else's escrow sits alongside -- and expect the guard's own error rather than any revert. The upgrade test upgrades to a version that APPENDS a field and reads the old ones back through the new layout, so a reordered field fails it; a byte-identical upgrade could not. Downstream: `Deploy.s.sol`, the vendored artifacts and `COVERED`, a new `bindings/escrow.rs`, the TypeScript ABI, and an anvil test that deploys the escrow, watches two spellings of one handle accumulate in a single slot and requires the authorization refusal by name. The mock verifier deliberately does NOT ship in the crate's artifacts: it reports whatever a caller stages, so a copy reachable from a deploy tool is a way to mint any identity on a live chain. Assisted-by: Claude Opus 5 Signed-off-by: SupremaLex --- .../HandleEscrow.sol/HandleEscrow.json | 25 + .../IdentityNames.sol/IdentityNames.json | 3 +- rust/contracts/src/artifacts.rs | 2 + rust/contracts/src/bindings/escrow.rs | 90 ++ rust/contracts/src/bindings/mod.rs | 1 + rust/contracts/tests/anvil.rs | 163 ++++ scripts/vendor-artifacts.sh | 2 + solidity/contracts/escrow/HandleEscrow.sol | 457 ++++++++++ solidity/contracts/escrow/IIdentityNames.sol | 29 + .../contracts/escrow/test/HandleEscrow.t.sol | 861 ++++++++++++++++++ solidity/contracts/identity/IdentityNames.sol | 21 + .../identity/test/IdentityNames.t.sol | 25 + solidity/script/Deploy.s.sol | 16 + ts/packages/contracts/scripts/codegen.mjs | 1 + .../contracts/src/abis/handleEscrow.ts | 690 ++++++++++++++ .../contracts/src/abis/identityNames.ts | 46 + ts/packages/contracts/src/abis/index.ts | 1 + 17 files changed, 2432 insertions(+), 1 deletion(-) create mode 100644 rust/contracts/artifacts/HandleEscrow.sol/HandleEscrow.json create mode 100644 rust/contracts/src/bindings/escrow.rs create mode 100644 solidity/contracts/escrow/HandleEscrow.sol create mode 100644 solidity/contracts/escrow/IIdentityNames.sol create mode 100644 solidity/contracts/escrow/test/HandleEscrow.t.sol create mode 100644 ts/packages/contracts/src/abis/handleEscrow.ts diff --git a/rust/contracts/artifacts/HandleEscrow.sol/HandleEscrow.json b/rust/contracts/artifacts/HandleEscrow.sol/HandleEscrow.json new file mode 100644 index 0000000..1cba11e --- /dev/null +++ b/rust/contracts/artifacts/HandleEscrow.sol/HandleEscrow.json @@ -0,0 +1,25 @@ +{ + "bytecode": { + "linkReferences": {}, + "object": "0x60a080604052346100c257306080525f516020611d325f395f51905f525460ff8160401c166100b3576002600160401b03196001600160401b03821601610060575b604051611c6b90816100c7823960805181818161038601526104290152f35b6001600160401b0319166001600160401b039081175f516020611d325f395f51905f525581527fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d290602090a15f80610041565b63f92ee8a960e01b5f5260045ffd5b5f80fdfe6080806040526004361015610012575f80fd5b5f3560e01c908163056da04814610b285750806314ac004614610ae0578063192d06e31461085e5780632b229bb9146108205780632b87fea01461079b578063485cc955146105b95780634f1ef286146103da57806352d1902d14610374578063715018a61461032b5780637967c6f0146102cd57806379ba50971461027f5780638da5cb5b1461024b578063a0cf0aea14610231578063ad3cb1cc146101ee578063c1473d3f14610193578063e30c39781461015f5763f2fde38b146100d7575f80fd5b3461015b57602036600319011261015b576100f0610c6b565b6100f8611605565b5f516020611c4b5f395f51905f5280546001600160a01b0319166001600160a01b039283169081179091555f516020611b8b5f395f51905f52549091167f38d16b8cac22d99fc7c124b9cd0de2d3fa1faef420bfe791d8c362d765e227005f80a3005b5f80fd5b3461015b575f36600319011261015b575f516020611c4b5f395f51905f52546040516001600160a01b039091168152602090f35b3461015b57604036600319011261015b576024356001600160a01b038116810361015b576004355f525f516020611bcb5f395f51905f5260205260405f209060018060a01b03165f52602052602060405f2054604051908152f35b3461015b575f36600319011261015b5761022d60405161020f604082610b57565b60058152640352e302e360dc1b602082015260405191829182610bfd565b0390f35b3461015b575f36600319011261015b5760206040515f8152f35b3461015b575f36600319011261015b575f516020611b8b5f395f51905f52546040516001600160a01b039091168152602090f35b3461015b575f36600319011261015b575f516020611c4b5f395f51905f5254336001600160a01b03909116036102ba576102b83361159a565b005b63118cdaa760e01b5f523360045260245ffd5b608036600319011261015b5760243567ffffffffffffffff811161015b576102fc610319913690600401610c27565b610304610c55565b9061030d6114a7565b60643592600435610d79565b5f5f516020611c0b5f395f51905f525d005b3461015b575f36600319011261015b5760405162461bcd60e51b81526020600482015260116024820152701c995b9bdd5b98d948191a5cd8589b1959607a1b6044820152606490fd5b3461015b575f36600319011261015b577f00000000000000000000000000000000000000000000000000000000000000006001600160a01b031630036103cb5760206040515f516020611bab5f395f51905f528152f35b63703e46dd60e11b5f5260045ffd5b604036600319011261015b576103ee610c6b565b60243567ffffffffffffffff811161015b573660238201121561015b5761041f903690602481600401359101610ba9565b6001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016308114908115610597575b506103cb57610461611605565b6040516352d1902d60e01b81526001600160a01b0383169290602081600481875afa5f9181610563575b506104a35783634c9c8ce360e01b5f5260045260245ffd5b805f516020611bab5f395f51905f528592036105515750813b1561053f575f516020611bab5f395f51905f5280546001600160a01b031916821790557fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b5f80a2815115610527575f808360206102b895519101845af46105216114dc565b91611b2c565b50503461053057005b63b398979f60e01b5f5260045ffd5b634c9c8ce360e01b5f5260045260245ffd5b632a87526960e21b5f5260045260245ffd5b9091506020813d60201161058f575b8161057f60209383610b57565b8101031261015b5751908561048b565b3d9150610572565b5f516020611bab5f395f51905f52546001600160a01b03161415905083610454565b3461015b57604036600319011261015b576105d2610c6b565b6024356001600160a01b0381169081900361015b575f516020611c2b5f395f51905f52549160ff8360401c16159267ffffffffffffffff811680159081610793575b6001149081610789575b159081610780575b506107715767ffffffffffffffff1981166001175f516020611c2b5f395f51905f525583610745575b5081156107365761065e61167d565b61066661167d565b6001600160a01b038116156107235761067e9061159a565b61068661167d565b61068e61167d565b61069661167d565b6bffffffffffffffffffffffff60a01b5f516020611beb5f395f51905f525416175f516020611beb5f395f51905f52556106cc57005b68ff0000000000000000195f516020611c2b5f395f51905f5254165f516020611c2b5f395f51905f52557fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d2602060405160018152a1005b631e4fbdf760e01b5f525f60045260245ffd5b6325c514cb60e11b5f5260045ffd5b68ffffffffffffffffff191668010000000000000001175f516020611c2b5f395f51905f52558361064f565b63f92ee8a960e01b5f5260045ffd5b90501585610626565b303b15915061061e565b859150610614565b3461015b57606036600319011261015b5760243567ffffffffffffffff811161015b576107ec6107d26107e4923690600401610c27565b92906107dc610c55565b933691610ba9565b600435610cd7565b5f525f516020611bcb5f395f51905f5260205260405f209060018060a01b03165f52602052602060405f2054604051908152f35b3461015b57604036600319011261015b5760243567ffffffffffffffff811161015b576108566107e46020923690600401610bdf565b604051908152f35b3461015b57608036600319011261015b5760043560243567ffffffffffffffff811161015b57610892903690600401610c27565b9061089b610c55565b606435939092906001600160a01b03851680860361015b576108bb6114a7565b80158015610ad7575b610ac5575061091a916020916108e46108de368488610ba9565b82610cd7565b9460018060a01b035f516020611beb5f395f51905f52541691604051958694859384936391ef5b5160e01b855260048501610cc0565b03915afa908115610aba575f91610a8b575b506001600160a01b0316338103610a755750805f525f516020611bcb5f395f51905f5260205260405f2060018060a01b0383165f5260205260405f2054918215610a52575f8281525f516020611bcb5f395f51905f52602090815260408083206001600160a01b0390941680845293909152812055917f8c18dc0a2c06d397feb6dd44b409d75c443e6cfbe8cd39e19619d7809ff9b874906109fa9084610a0f576109d7818761150b565b604080516001600160a01b03909716875260208701919091523395918291820190565b0390a45f5f516020611c0b5f395f51905f525d005b60405163a9059cbb60e01b60208201526001600160a01b038716602482015260448082018390528152610a4d90610a47606482610b57565b86611625565b6109d7565b630c946d8360e01b5f9081526004929092526001600160a01b0316602452604490fd5b63b6bd8e8360e01b5f526004523360245260445ffd5b610aad915060203d602011610ab3575b610aa58183610b57565b810190610c81565b8461092c565b503d610a9b565b6040513d5f823e3d90fd5b631ecdd20960e31b5f5260045260245ffd5b503081146108c4565b3461015b57602036600319011261015b5760043567ffffffffffffffff811161015b57610b1c610b1761022d923690600401610bdf565b61131b565b60405191829182610bfd565b3461015b575f36600319011261015b575f516020611beb5f395f51905f52546001600160a01b03168152602090f35b90601f8019910116810190811067ffffffffffffffff821117610b7957604052565b634e487b7160e01b5f52604160045260245ffd5b67ffffffffffffffff8111610b7957601f01601f191660200190565b929192610bb582610b8d565b91610bc36040519384610b57565b82948184528183011161015b578281602093845f960137010152565b9080601f8301121561015b57816020610bfa93359101610ba9565b90565b602060409281835280519182918282860152018484015e5f828201840152601f01601f1916010190565b9181601f8401121561015b5782359167ffffffffffffffff831161015b576020838186019501011161015b57565b604435906001600160a01b038216820361015b57565b600435906001600160a01b038216820361015b57565b9081602091031261015b57516001600160a01b038116810361015b5790565b908060209392818452848401375f828201840152601f01601f1916010190565b604090610bfa949281528160208201520191610ca0565b610d1a601b6020604051610cec604082610b57565b828152017f6c696269642e657363726f772e68616e646c652d736c6f742e763100000000008152209261131b565b6020815191012060405191602083019384526040830152606082015260608152610d45608082610b57565b51902090565b91908203918211610d5857565b634e487b7160e01b5f52601160045260245ffd5b91908201809211610d5857565b939291938315610f85575f516020611beb5f395f51905f5254604051630f62983f60e41b8152600481018390526001600160a01b03909116949060a081602481895afa8015610aba575f906111f4575b610dde9150610dd9368987610ba9565b6116a8565b5060058110156111e057806111ce5750610e02610dfc368886610ba9565b83610cd7565b6001600160a01b039094169485159081156111b15782340361119a575b602060405180926391ef5b5160e01b82528180610e418d8b8b60048501610cc0565b03915afa908115610aba575f9161117b575b506001600160a01b0381169081610ffe57505015610ee7577fee495663dac7b04af757ba8ee849872169d884c3d001f57fa2352898ce71f7aa92610ed991965b855f525f516020611bcb5f395f51905f5260205260405f20875f5260205260405f20610ec0898254610d6c565b9055604051938452606060208501526060840191610ca0565b9460408201528033950390a4565b6040516370a0823160e01b815230600482015290602082602481895afa918215610aba575f92610fc8575b50610f1f9030338861154f565b6040516370a0823160e01b815230600482015290602082602481895afa8015610aba575f90610f94575b610f539250610d4b565b948515610f85577fee495663dac7b04af757ba8ee849872169d884c3d001f57fa2352898ce71f7aa92610ed991610e93565b631f2a200560e01b5f5260045ffd5b506020823d602011610fc0575b81610fae60209383610b57565b8101031261015b57610f539151610f49565b3d9150610fa1565b9091506020813d602011610ff6575b81610fe460209383610b57565b8101031261015b575190610f1f610f12565b3d9150610fd7565b83919594929893985f14611068579161103e61105a94927f276c5feea4a110ef0c00982b96bce8903321dfc84e414bdf4e898fad7ecfb5cc97969461150b565b6040519485526020850152608060408501526080840191610ca0565b9460608201528033950390a4565b949392919750604051946370a0823160e01b86528460048701526020866024818b5afa958615610aba575f96611143575b50906110a691338961154f565b6040516370a0823160e01b815260048101849052936020856024818a5afa948515610aba575f95611109575b509061110361105a93927f276c5feea4a110ef0c00982b96bce8903321dfc84e414bdf4e898fad7ecfb5cc96610d4b565b9761103e565b945091906020853d60201161113b575b8161112660209383610b57565b8101031261015b5793519390916111036110d2565b3d9150611119565b919095506020823d602011611173575b8161116060209383610b57565b8101031261015b579051946110a6611099565b3d9150611153565b611194915060203d602011610ab357610aa58183610b57565b5f610e53565b82630626ade360e41b5f526004523460245260445ffd5b3415610e1f57630626ade360e41b5f525f6004523460245260445ffd5b63eb1fffd960e01b5f5260045260245ffd5b634e487b7160e01b5f52602160045260245ffd5b5060a0813d60a011611290575b8161120e60a09383610b57565b8101031261015b5760405160a0810181811067ffffffffffffffff821117610b795760405281519161ffff8316830361015b57608061128691610dde94845261125960208201611542565b602085015261126a60408201611542565b604085015261127b60608201611542565b606085015201611542565b6080820152610dc9565b3d9150611201565b9081518110156112a9570160200190565b634e487b7160e01b5f52603260045260245ffd5b5f198114610d585760010190565b8015610d58575f190190565b906112e182610b8d565b6112ee6040519182610b57565b82815280926112ff601f1991610b8d565b0190602036910137565b60ff60209116019060ff8211610d5857565b80515f5b81811080611486575b1561133b57611336906112bd565b61131f565b905b81811180611458575b1561135957611354906112cb565b61133d565b91909180831080611437575b611422575b8261137491610d4b565b90811561140e57611384826112d7565b925f5b838110611395575050505090565b806113ab6113a560019385610d6c565b85611298565b516001600160f81b0319811690604160f81b82101580611400575b6113df575b505f1a6113d88288611298565b5301611387565b6001600160f81b031991506113f69060f81c611309565b60f81b165f6113cb565b50602d60f91b8211156113c6565b63eb1fffd960e01b5f52600160045260245ffd5b9161142f611374916112bd565b92905061136a565b50600160fe1b6001600160f81b03196114508585611298565b511614611365565b505f198101818111610d5857600160fd1b906001600160f81b03199061147e9086611298565b511614611346565b50600160fd1b6001600160f81b031961149f8386611298565b511614611328565b5f516020611c0b5f395f51905f525c6114cd5760015f516020611c0b5f395f51905f525d565b633ee5aeb560e01b5f5260045ffd5b3d15611506573d906114ed82610b8d565b916114fb6040519384610b57565b82523d5f602084013e565b606090565b5f80808085855af161151b6114dc565b5015611525575050565b63296c17bb60e21b5f5260018060a01b031660045260245260445ffd5b5190811515820361015b57565b6040516323b872dd60e01b60208201526001600160a01b03928316602482015292909116604483015260648083019390935291815261159891611593608483610b57565b611625565b565b5f516020611c4b5f395f51905f5280546001600160a01b03199081169091555f516020611b8b5f395f51905f5280549182166001600160a01b0393841690811790915591167f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e05f80a3565b5f516020611b8b5f395f51905f52546001600160a01b031633036102ba57565b905f602091828151910182855af115610aba575f513d61167457506001600160a01b0381163b155b6116545750565b635274afe760e01b5f9081526001600160a01b0391909116600452602490fd5b6001141561164d565b60ff5f516020611c2b5f395f51905f525460401c161561169957565b631afcd79f60e31b5f5260045ffd5b915f918351925b838110806118d9575b156116cb576116c6906112bd565b6116af565b93925b848111806118ab575b156116ea576116e5906112cb565b6116ce565b9290919260208301511515806118a2575b80611881575b61186c575b8461171091610d4b565b9081156118565761ffff83511682116118405761172c826112d7565b945f5b83811061179d5750505050604081015115611771575061174e82611ac2565b15611759575b5f9190565b6040516004925061176b602082610b57565b5f815290565b6080015161177e575f9190565b611787826119f9565b611754576040516004925061176b602082610b57565b6117b06117aa8284610d6c565b84611298565b516001600160f81b0319811690604160f81b82101580611832575b611811575b506117db86826118fa565b156117f757906001915f1a6117f0828a611298565b530161172f565b5050505050509050600390602061176b6040519182610b57565b6001600160f81b031991506118289060f81c611309565b60f81b165f6117d0565b50602d60f91b8211156117cb565b505050905060029060405161176b602082610b57565b505050905060019060405161176b602082610b57565b93611879611710916112bd565b949050611706565b50600160fe1b6001600160f81b031961189a8785611298565b511614611701565b508481116116fb565b505f198101818111610d5857600160fd1b906001600160f81b0319906118d19087611298565b5116146116d7565b50600160fd1b6001600160f81b03196118f28388611298565b5116146116b8565b6001600160f81b03191690606160f81b821015806119eb575b61196c57600360fc1b821015806119dd575b61196c576040810151611980576060810151151580611973575b61196c57608001511515908161195e575b50611959575f90565b600190565b602d60f81b1490505f611950565b5050600190565b50605f60f81b821461193f565b50601760f91b81149081156119cf575b81156119c1575b81156119b3575b81156119a8575090565b600160fe1b14919050565b605f60f81b8114915061199e565b602d60f81b81149150611997565b602b60f81b81149150611990565b50603960f81b821115611925565b50603d60f91b821115611913565b8051156112a95760208101516001600160f81b031916602d60f81b148015611a92575b611a8d5760015b815181101561196c57602d60f81b6001600160f81b0319611a448385611298565b51161480611a5f575b611a5957600101611a23565b50505f90565b505f198101818111610d5857602d60f81b906001600160f81b031990611a859085611298565b511614611a4d565b505f90565b5080515f198101908111610d5857602d60f81b906001600160f81b031990611aba9084611298565b511614611a1c565b5f195f5b8251811015611b0357600160fe1b6001600160f81b0319611ae78386611298565b511614611af7575b600101611ac6565b9019611a595780611aef565b505f198114611a59578015159182611b1a57505090565b515f19810192508211610d5857141590565b90611b505750805115611b4157805190602001fd5b63d6bda27560e01b5f5260045ffd5b81511580611b81575b611b61575090565b639996b31560e01b5f9081526001600160a01b0391909116600452602490fd5b50803b15611b5956fe9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbcfcca8d7d2c66f78c2760f3fcd99e0bf938b0aeb0d0b471f481dd50b8aff6b400fcca8d7d2c66f78c2760f3fcd99e0bf938b0aeb0d0b471f481dd50b8aff6b4019b779b17422d0df92223018b32b4d1fa46e071723d6817e2486d003becc55f00f0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00237e158222e3e6968b72b9db0d8043aacf074ad9f650f0d1606b4d82ee432c00f0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00" + }, + "methodIdentifiers": { + "NATIVE()": "a0cf0aea", + "UPGRADE_INTERFACE_VERSION()": "ad3cb1cc", + "acceptOwnership()": "79ba5097", + "canonicalHandle(string)": "14ac0046", + "claim(bytes32,string,address,address)": "192d06e3", + "deposit(bytes32,string,address,uint256)": "7967c6f0", + "escrowed(bytes32,string,address)": "2b87fea0", + "escrowedAt(bytes32,address)": "c1473d3f", + "initialize(address,address)": "485cc955", + "names()": "056da048", + "owner()": "8da5cb5b", + "pendingOwner()": "e30c3978", + "proxiableUUID()": "52d1902d", + "renounceOwnership()": "715018a6", + "slotOf(bytes32,string)": "2b229bb9", + "transferOwnership(address)": "f2fde38b", + "upgradeToAndCall(address,bytes)": "4f1ef286" + } +} diff --git a/rust/contracts/artifacts/IdentityNames.sol/IdentityNames.json b/rust/contracts/artifacts/IdentityNames.sol/IdentityNames.json index c92982c..da9ee97 100644 --- a/rust/contracts/artifacts/IdentityNames.sol/IdentityNames.json +++ b/rust/contracts/artifacts/IdentityNames.sol/IdentityNames.json @@ -1,7 +1,7 @@ { "bytecode": { "linkReferences": {}, - "object": "0x60a080604052346100c257306080525f516020612c4a5f395f51905f525460ff8160401c166100b3576002600160401b03196001600160401b03821601610060575b604051612b8390816100c7823960805181818161168e01526118120152f35b6001600160401b0319166001600160401b039081175f516020612c4a5f395f51905f525581527fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d290602090a15f80610041565b63f92ee8a960e01b5f5260045ffd5b5f80fdfe60806040526004361015610011575f80fd5b5f3560e01c80631340305a14611da8578063284e6abf14611ca45780632de00c0114611aec57806338e09e7814611ad15780633b31fcd114611a2d5780634a9252cf146119c95780634f1ef286146117c4578063502927d8146116e257806352d1902d1461167c57806358b9b1e71461113e578063606bf407146110e757806361d5a73114610954578063621fa08914610915578063715018a6146108cc57806379ba50971461087e578063839692bc146108275780638da5cb5b146107f357806391ef5b5114610781578063a43f59661461067b578063a69460311461056c578063ad3cb1cc14610525578063b1b805b514610403578063bf89efd21461039a578063c4d66de81461021b578063e30c3978146101e7578063ec8efd21146101ca5763f2fde38b14610142575f80fd5b346101c65760203660031901126101c65761015b611dd0565b6101636122c9565b5f516020612b635f395f51905f5280546001600160a01b0319166001600160a01b039283169081179091555f516020612ac35f395f51905f52549091167f38d16b8cac22d99fc7c124b9cd0de2d3fa1faef420bfe791d8c362d765e227005f80a3005b5f80fd5b346101c6575f3660031901126101c6576020604051620151808152f35b346101c6575f3660031901126101c6575f516020612b635f395f51905f52546040516001600160a01b039091168152602090f35b346101c65760203660031901126101c657610234611dd0565b5f516020612b435f395f51905f52549060ff8260401c1615916001600160401b03811680159081610392575b6001149081610388575b15908161037f575b506103705767ffffffffffffffff1981166001175f516020612b435f395f51905f525582610344575b506102a46127a7565b6102ac6127a7565b6001600160a01b03811615610331576102c49061232f565b6102cc6127a7565b6102d46127a7565b6102da57005b68ff0000000000000000195f516020612b435f395f51905f5254165f516020612b435f395f51905f52557fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d2602060405160018152a1005b631e4fbdf760e01b5f525f60045260245ffd5b68ffffffffffffffffff191668010000000000000001175f516020612b435f395f51905f52558261029b565b63f92ee8a960e01b5f5260045ffd5b90501584610272565b303b15915061026a565b849150610260565b346101c65760203660031901126101c6576004355f9081525f516020612a835f395f51905f5260209081526040918290205482516001600160a01b038216815260a082901c6001600160401b03169281019290925260e01c9181019190915280606081015b0390f35b346101c65760c03660031901126101c65760043560a03660231901126101c65761042b6122c9565b805f525f516020612a635f395f51905f5260205260405f2060243561ffff81168091036101c65781549060443590811515918281036101c6575060643590811515918281036101c65762ff00009263ff000000915060181b169363ffffffff1916179160101b1617178155608435801515908181036101c6575064ff0000000082549160201b169064ff00000000191617815560a435908115158083036101c657815465ff0000000000191660289190911b65ff000000000016178155600101805464ff000000001916640100000000179055507fb01c0a30cea93f12bdaa4228f3ced17148be2cc93045c1848d99d588c0141c8f5f80a2005b346101c6575f3660031901126101c6576103ff604051610546604082611eb6565b60058152640352e302e360dc1b6020820152604051918291602083526020830190611de6565b346101c65760403660031901126101c65760043563ffffffff61058d611e37565b6105956122c9565b1690811561066c57805f525f516020612a635f395f51905f5260205260ff600160405f20015460201c161561065a575f8181525f516020612ae35f395f51905f52602090815260408083208584529091529020546001600160a01b03161561064557805f525f516020612a635f395f51905f52602052600160405f20018263ffffffff198254161790557f39de5aa871268e261cc41a46876cebce3b7b112a7b158b360f850538860a9bab5f80a3005b6312eaaba960e21b5f5260045260245260445ffd5b6313f6864d60e01b5f5260045260245ffd5b635498399960e11b5f5260045ffd5b346101c65760403660031901126101c657600435610697611e37565b906106a06122c9565b5f8181525f516020612ae35f395f51905f526020908152604080832063ffffffff861684529091529020546001600160a01b03161561076457805f525f516020612a635f395f51905f5260205263ffffffff80600160405f20015416921680921461074f57805f525f516020612ae35f395f51905f5260205260405f20825f526020525f60408120557f2af3e10bdbb7f8ceef8368a212ed59736df2eb37120a7ef435c48d980c04480a5f80a3005b637d788d8f60e11b5f5260045260245260445ffd5b9063ffffffff916312eaaba960e21b5f526004521660245260445ffd5b346101c6576107af6107a961079536611f28565b92906107a08361219f565b51933691611ef2565b90612171565b90156107ea575f525f516020612b235f395f51905f52602052602060018060a01b0360405f2054165b6040516001600160a01b039091168152f35b5060205f6107d8565b346101c6575f3660031901126101c6575f516020612ac35f395f51905f52546040516001600160a01b039091168152602090f35b346101c65760403660031901126101c657610840611e37565b6004355f525f516020612ae35f395f51905f5260205263ffffffff60405f2091165f52602052602060405f2060018060a01b03905416604051908152f35b346101c6575f3660031901126101c6575f516020612b635f395f51905f5254336001600160a01b03909116036108b9576108b73361232f565b005b63118cdaa760e01b5f523360045260245ffd5b346101c6575f3660031901126101c65760405162461bcd60e51b81526020600482015260116024820152701c995b9bdd5b98d948191a5cd8589b1959607a1b6044820152606490fd5b346101c65760203660031901126101c6576004355f525f516020612a635f395f51905f52602052602063ffffffff600160405f20015416604051908152f35b346101c65760603660031901126101c6576004356024356001600160401b0381116101c657610987903690600401611e0a565b9060443592831591821585036101c6576109a08261219f565b9263ffffffff60208501511691835f525f516020612ae35f395f51905f5260205260405f2063ffffffff84165f5260205260405f2090604051916109e383611e4a565b549160018060a01b0383168082526001600160401b03602083019460a01c168452156110d0575160405163473b057f60e11b81526020600482015260248101899052975f9289926001600160a01b031691839160449183918190838501378181018301879052601f01601f191681010301915afa9586156110c5575f96610fff575b506040860180516001600160a01b031615610ff057516001600160a01b0316338103610fda5750606086016001600160401b0381511615610fcb5786515115610fbc576001600160401b038151166001600160401b038351166001600160401b034216016001600160401b038111610f87576001600160401b031690818111610fa75750505190516001600160401b03908116911681811115610f9b57036001600160401b038111610f8757610b2490945b60208701519051906123cd565b906005811015610f735760018114610f645760028114610f555760038114610f4657600414610f3757610b5886518561224a565b95610b63828661263a565b97875f525f516020612a835f395f51905f52602052610b926001600160401b0360405f205460a01c16886126a5565b885f525f516020612b235f395f51905f52602052610bc06001600160401b0360405f205460a01c16886126a5565b610c64604051610bcf81611e80565b3381526001600160401b03989098166020808a018281526040808c018a81525f8e81525f516020612a835f395f51905f5290945292209a518b5491516001600160e01b03199092166001600160a01b03919091161760a09190911b67ffffffffffffffff60a01b16178a5590989063ffffffff905b5182546001600160e01b0316911660e01b6001600160e01b031916179055565b610cdd604051610c7381611e80565b33815260208082018a815260408084018a81525f8f81525f516020612b235f395f51905f5290945292209251835491516001600160e01b03199092166001600160a01b03919091161760a09190911b67ffffffffffffffff60a01b1617825563ffffffff90610c44565b610ce88989886126d2565b875f527f064503501234cc9c6e116cf4a84c07475158dabb6a3dcee437a89227e23bf2046020528860405f2055885f527f064503501234cc9c6e116cf4a84c07475158dabb6a3dcee437a89227e23bf2056020528760405f205592610f11575b82610da7575b5f516020612aa35f395f51905f5294939291610d7f610d8d925160405196875260c0602088015260c0870190611de6565b908582036040870152611de6565b9460608401521515608083015260a08201528033930390a4005b610db033611f5b565b855f5260205260405f2082516001600160401b038111610efd57610dd48254611f93565b601f8111610eba575b506020601f8211600114610e4057925f516020612aa35f395f51905f529796959492610e2583610d7f94610d8d975f91610e35575b508160011b915f199060031b1c19161790565b90555b9250509192939450610d4e565b90508601518f610e12565b601f19821690835f52805f20915f5b818110610ea2575083610d8d96935f516020612aa35f395f51905f529b9a99989693610d7f9660019410610e8a575b5050811b019055610e28565b8701515f1960f88460031b161c191690558e80610e7e565b9192602060018192868b015181550194019201610e4f565b81811115610ddd57610eef90835f5260205f2090601f840160051c9060208510610ef5575b601f82910160051c039101612156565b8a610ddd565b5f9150610edf565b634e487b7160e01b5f52604160045260245ffd5b9150610f1c33611f5b565b845f52602052610f2f60405f2054611f93565b151591610d48565b63a9b0b66560e01b5f5260045ffd5b634bd5d0ad60e01b5f5260045ffd5b630183219160e51b5f5260045ffd5b6375ebfc4960e01b5f5260045ffd5b634e487b7160e01b5f52602160045260245ffd5b634e487b7160e01b5f52601160045260245ffd5b5050610b245f94610b17565b63182f1ac760e21b5f5260045260245260445ffd5b63eae8cfd360e01b5f5260045ffd5b634f1a06e760e11b5f5260045ffd5b634dab16af60e11b5f526004523360245260445ffd5b633c88bf6360e01b5f5260045ffd5b9095503d805f833e6110118183611eb6565b8101906020818303126101c6578051906001600160401b0382116101c657016080818303126101c6576040519161104783611e9b565b81516001600160401b0381116101c657816110639184016122e9565b83526020820151906001600160401b0382116101c6576110849183016122e9565b60208301526040810151906001600160a01b03821682036101c657606091604084015201516001600160401b03811681036101c65760608201529487610a65565b6040513d5f823e3d90fd5b84866312eaaba960e21b5f5260045260245260445ffd5b346101c65761111461110e6110fb36611f28565b61110683949361219f565b503691611ef2565b9061224a565b5f525f516020612a835f395f51905f52602052602060018060a01b0360405f205416604051908152f35b346101c65760803660031901126101c65760043561115a611e37565b906044356001600160401b0381116101c65761117a903690600401611e0a565b9160643593841590811586036101c65763ffffffff1690811561066c576111a08361219f565b93835f525f516020612ae35f395f51905f5260205260405f20835f5260205260405f2090604051916111d183611e4a565b549160018060a01b0383168082526001600160401b03602083019460a01c168452156110d0575160405163473b057f60e11b81526020600482015260248101899052975f9289926001600160a01b031691839160449183918190838501378181018301879052601f01601f191681010301915afa9586156110c5575f966115b6575b506040860180516001600160a01b031615610ff057516001600160a01b0316338103610fda5750606086016001600160401b0381511615610fcb5786515115610fbc576001600160401b038151166001600160401b038351166001600160401b034216016001600160401b038111610f87576001600160401b031690818111610fa75750505190516001600160401b039081169116818111156115aa57036001600160401b038111610f8757611311909460208701519051906123cd565b906005811015610f735760018114610f645760028114610f555760038114610f4657600414610f375761134586518561224a565b95611350828661263a565b97875f525f516020612a835f395f51905f5260205261137f6001600160401b0360405f205460a01c16886126a5565b885f525f516020612b235f395f51905f526020526113ad6001600160401b0360405f205460a01c16886126a5565b6113bc604051610bcf81611e80565b6113cb604051610c7381611e80565b6113d68989886126d2565b875f527f064503501234cc9c6e116cf4a84c07475158dabb6a3dcee437a89227e23bf2046020528860405f2055885f527f064503501234cc9c6e116cf4a84c07475158dabb6a3dcee437a89227e23bf2056020528760405f205592611584575b8261146c575f516020612aa35f395f51905f5294939291610d7f610d8d925160405196875260c0602088015260c0870190611de6565b61147533611f5b565b855f5260205260405f2082516001600160401b038111610efd576114998254611f93565b601f811161154a575b506020601f82116001146114e957925f516020612aa35f395f51905f529796959492610e2583610d7f94610d8d975f91610e3557508160011b915f199060031b1c19161790565b601f19821690835f52805f20915f5b818110611532575083610d8d96935f516020612aa35f395f51905f529b9a99989693610d7f9660019410610e8a575050811b019055610e28565b9192602060018192868b0151815501940192016114f8565b818111156114a25761157e90835f5260205f2090601f840160051c9060208510610ef557601f82910160051c039101612156565b8a6114a2565b915061158f33611f5b565b845f526020526115a260405f2054611f93565b151591611436565b50506113115f94610b17565b9095503d805f833e6115c88183611eb6565b8101906020818303126101c6578051906001600160401b0382116101c657016080818303126101c657604051916115fe83611e9b565b81516001600160401b0381116101c6578161161a9184016122e9565b83526020820151906001600160401b0382116101c65761163b9183016122e9565b60208301526040810151906001600160a01b03821682036101c657606091604084015201516001600160401b03811681036101c65760608201529487611253565b346101c6575f3660031901126101c6577f00000000000000000000000000000000000000000000000000000000000000006001600160a01b031630036116d35760206040515f516020612b035f395f51905f528152f35b63703e46dd60e11b5f5260045ffd5b346101c65760403660031901126101c6576117036116fe611dd0565b611f5b565b6024355f5260205260405f20604051905f9080549061172182611f93565b808552916001811690811561179d575060011461175d575b6103ff8461174981860382611eb6565b604051918291602083526020830190611de6565b5f90815260208120939250905b8082106117835750909150810160200161174982611739565b91926001816020925483858801015201910190929161176a565b60ff191660208087019190915292151560051b850190920192506117499150839050611739565b60403660031901126101c6576117d8611dd0565b6024356001600160401b0381116101c657366023820112156101c657611808903690602481600401359101611ef2565b6001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000163081149081156119a7575b506116d35761184a6122c9565b6040516352d1902d60e01b81526001600160a01b0383169290602081600481875afa5f9181611973575b5061188c5783634c9c8ce360e01b5f5260045260245ffd5b805f516020612b035f395f51905f528592036119615750813b1561194f575f516020612b035f395f51905f5280546001600160a01b031916821790557fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b5f80a2815115611937575f808360206108b795519101845af43d1561192f573d9161191383611ed7565b926119216040519485611eb6565b83523d5f602085013e612a04565b606091612a04565b50503461194057005b63b398979f60e01b5f5260045ffd5b634c9c8ce360e01b5f5260045260245ffd5b632a87526960e21b5f5260045260245ffd5b9091506020813d60201161199f575b8161198f60209383611eb6565b810103126101c657519085611874565b3d9150611982565b5f516020612b035f395f51905f52546001600160a01b0316141590508361183d565b346101c65760203660031901126101c6576004355f9081525f516020612b235f395f51905f5260209081526040918290205482516001600160a01b03821681526001600160401b0360a083901c169281019290925260e01c91810191909152606090f35b346101c65760203660031901126101c657600435611a4a33611f5b565b815f5260205260405f20611a5e8154611f93565b9081611a8d575b82337f8e43ba98be6948052c3368d53fef5505611df0ae5994d278a646eb9ad3479edd5f80a3005b81601f5f9311600114611aa45750555b8180611a65565b81835260208320611ac191601f0160051c84190190600101612156565b8082528160208120915555611a9d565b346101c6575f3660031901126101c657602060405160018152f35b346101c65760803660031901126101c657600435611b08611e37565b6044356001600160a01b0381169291908390036101c657606435906001600160401b0382168092036101c65763ffffffff90611b426122c9565b1692831561066c578015611c9557620151808211611c7b57825f525f516020612a635f395f51905f52602052600160405f20019160ff835460201c1615611c68577f13b9f8bb5ee05a42b59cc1d4197d78c5ca07a7236ce1bd191161faca7e5bb398604086938693611c1b8351611bb881611e4a565b83815260208082018481525f8981525f516020612ae35f395f51905f5283528781208b8252909252908690209151825491516001600160e01b03199092166001600160a01b03919091161760a09190911b67ffffffffffffffff60a01b16179055565b82519182526020820152a363ffffffff81541615611c3557005b805463ffffffff1916831790557f39de5aa871268e261cc41a46876cebce3b7b112a7b158b360f850538860a9bab5f80a3005b836313f6864d60e01b5f5260045260245ffd5b5063911d6f1360e01b5f526004526201518060245260445ffd5b6379e2e00360e01b5f5260045ffd5b346101c65760603660031901126101c6576004356024356001600160401b0381116101c657611cd7903690600401611e0a565b91604435906001600160401b0382116101c65761110e611d1d93611d23604096611d08611d54963690600401611e0a565b979093611d148761219f565b51923691611ef2565b85612171565b9015611da0575f9081525f516020612b235f395f51905f52602052869020546001600160a01b0316945b3691611ef2565b5f9081525f516020612a835f395f51905f52602052829020546001600160a01b03918216918215159183911682611d96575b5050825191825215156020820152f35b1490508184611d86565b505f94611d4d565b346101c65760403660031901126101c6576103ff611749611dc7611dd0565b60243590612018565b600435906001600160a01b03821682036101c657565b805180835260209291819084018484015e5f828201840152601f01601f1916010190565b9181601f840112156101c6578235916001600160401b0383116101c657602083818601950101116101c657565b6024359063ffffffff821682036101c657565b604081019081106001600160401b03821117610efd57604052565b60a081019081106001600160401b03821117610efd57604052565b606081019081106001600160401b03821117610efd57604052565b608081019081106001600160401b03821117610efd57604052565b90601f801991011681019081106001600160401b03821117610efd57604052565b6001600160401b038111610efd57601f01601f191660200190565b929192611efe82611ed7565b91611f0c6040519384611eb6565b8294818452818301116101c6578281602093845f960137010152565b9060406003198301126101c65760043591602435906001600160401b0382116101c657611f5791600401611e0a565b9091565b6001600160a01b03165f9081527f064503501234cc9c6e116cf4a84c07475158dabb6a3dcee437a89227e23bf2026020526040902090565b90600182811c92168015611fc1575b6020831014611fad57565b634e487b7160e01b5f52602260045260245ffd5b91607f1691611fa2565b90604051611fd881611e65565b608060ff82945461ffff81168452818160101c1615156020850152818160181c1615156040850152818160201c161515606085015260281c161515910152565b61202181611f5b565b825f5260205260405f209160405192835f82549261203e84611f93565b808452936001811690811561213457506001146120f0575b5061206392500384611eb6565b8251156120df5780612095915f525f516020612a635f395f51905f526020528361208f60405f20611fcb565b91612171565b90156120df575f9081525f516020612b235f395f51905f5260205260409020546001600160a01b039081169116036120ca5790565b506040516120d9602082611eb6565b5f815290565b5050506040516120d9602082611eb6565b90505f9291925260205f20905f915b818310612118575050906020612063928201015f612056565b6020919350806001915483858a010152019101909185926120ff565b90506020925061206394915060ff191682840152151560051b8201015f612056565b5f5b82811061216457505050565b5f82820155600101612158565b9161217b916123cd565b906005811015610f7357612197576121929161263a565b600191565b50505f905f90565b905f604080516121ae81611e80565b81516121b981611e65565b838152836020820152838382015283606082015283608082015281528260208201520152815f525f516020612a635f395f51905f5260205260405f209160016040519361220585611e80565b61220e81611fcb565b855201549260ff63ffffffff85169485602084015260201c1615938415918260408201529491612241575b5061065a5750565b9050155f612239565b6122c36019602060405161225f604082611eb6565b828152017f6479616b612e6964656e746974792e69642d6e6f64652e7631000000000000008152209260208151910120916122b56040519384926020840196876040919493926060820195825260208201520152565b03601f198101835282611eb6565b51902090565b5f516020612ac35f395f51905f52546001600160a01b031633036108b957565b81601f820112156101c65780519061230082611ed7565b9261230e6040519485611eb6565b828452602083830101116101c657815f9260208093018386015e8301015290565b5f516020612b635f395f51905f5280546001600160a01b03199081169091555f516020612ac35f395f51905f5280549182166001600160a01b0393841690811790915591167f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e05f80a3565b9081518110156123ab570160200190565b634e487b7160e01b5f52603260045260245ffd5b5f198114610f875760010190565b80519290915f5b84811080612619575b156123f0576123eb906123bf565b6123d4565b92935b838111806125eb575b1561240f578015610f87575f19016123f3565b9091929360208301511515806125e2575b806125c1575b6125b1575b84820391808311610f8757851461259b5761ffff83511682116125855761245182611ed7565b9461245f6040519687611eb6565b828652601f1961246e84611ed7565b013660208801375f5b8381106124df57505050506040810151156124b357506124968261299a565b156124a1575b5f9190565b604051600492506120d9602082611eb6565b608001516124c0575f9190565b6124c9826128d1565b61249c57604051600492506120d9602082611eb6565b808201808311610f87576124f3908461239a565b516001600160f81b0319811690604160f81b82101580612577575b612554575b5061251e86826127d2565b1561253a57906001915f1a612533828a61239a565b5301612477565b505050505050905060039060206120d96040519182611eb6565b6020915060f81c0160ff8111610f875760f81b6001600160f81b0319165f612513565b50602d60f91b82111561250e565b50505090506002906040516120d9602082611eb6565b50505090506001906040516120d9602082611eb6565b936125bb906123bf565b9361242b565b50600160fe1b6001600160f81b03196125da878461239a565b511614612426565b50848211612420565b505f198101818111610f8757600160fd1b906001600160f81b031990612611908861239a565b5116146123fc565b50600160fd1b6001600160f81b0319612632838761239a565b5116146123dd565b6122c3601d602060405161264f604082611eb6565b828152017f6479616b612e6964656e746974792e68616e646c652d6e6f64652e76310000008152209260208151910120916122b56040519384926020840196876040919493926060820195825260208201520152565b6001600160401b039182169116818111156126be575050565b6277f1a160e81b5f5260045260245260445ffd5b9190805f527f064503501234cc9c6e116cf4a84c07475158dabb6a3dcee437a89227e23bf20460205260405f205491821590811561279d575b5061279857815f527f064503501234cc9c6e116cf4a84c07475158dabb6a3dcee437a89227e23bf20560205260405f205403612794575f8181525f516020612b235f395f51905f5260205260408120805467ffffffffffffffff60a01b1690553392907f278e2122d24782b703f05416ac53a9750d67bcf6fa1603b94ec95660821a89149080a4565b5050565b505050565b905082145f61270b565b60ff5f516020612b435f395f51905f525460401c16156127c357565b631afcd79f60e31b5f5260045ffd5b6001600160f81b03191690606160f81b821015806128c3575b61284457600360fc1b821015806128b5575b61284457604081015161285857606081015115158061284b575b612844576080015115159081612836575b50612831575f90565b600190565b602d60f81b1490505f612828565b5050600190565b50605f60f81b8214612817565b50601760f91b81149081156128a7575b8115612899575b811561288b575b8115612880575090565b600160fe1b14919050565b605f60f81b81149150612876565b602d60f81b8114915061286f565b602b60f81b81149150612868565b50603960f81b8211156127fd565b50603d60f91b8211156127eb565b8051156123ab5760208101516001600160f81b031916602d60f81b14801561296a575b6129655760015b815181101561284457602d60f81b6001600160f81b031961291c838561239a565b51161480612937575b612931576001016128fb565b50505f90565b505f198101818111610f8757602d60f81b906001600160f81b03199061295d908561239a565b511614612925565b505f90565b5080515f198101908111610f8757602d60f81b906001600160f81b031990612992908461239a565b5116146128f4565b5f195f5b82518110156129db57600160fe1b6001600160f81b03196129bf838661239a565b5116146129cf575b60010161299e565b901961293157806129c7565b505f1981146129315780151591826129f257505090565b515f19810192508211610f8757141590565b90612a285750805115612a1957805190602001fd5b63d6bda27560e01b5f5260045ffd5b81511580612a59575b612a39575090565b639996b31560e01b5f9081526001600160a01b0391909116600452602490fd5b50803b15612a3156fe064503501234cc9c6e116cf4a84c07475158dabb6a3dcee437a89227e23bf203064503501234cc9c6e116cf4a84c07475158dabb6a3dcee437a89227e23bf2008acbd4495dbb2ff215c4799cd4551da5688633536a4c5381ff06eaca80119a2e9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300064503501234cc9c6e116cf4a84c07475158dabb6a3dcee437a89227e23bf206360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc064503501234cc9c6e116cf4a84c07475158dabb6a3dcee437a89227e23bf201f0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00237e158222e3e6968b72b9db0d8043aacf074ad9f650f0d1606b4d82ee432c00f0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00" + "object": "0x60a080604052346100c257306080525f516020612cc75f395f51905f525460ff8160401c166100b3576002600160401b03196001600160401b03821601610060575b604051612c0090816100c782396080518181816116fe01526118820152f35b6001600160401b0319166001600160401b039081175f516020612cc75f395f51905f525581527fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d290602090a15f80610041565b63f92ee8a960e01b5f5260045ffd5b5f80fdfe60806040526004361015610011575f80fd5b5f3560e01c80631340305a14611e18578063284e6abf14611d145780632de00c0114611b5c57806338e09e7814611b415780633b31fcd114611a9d5780634a9252cf14611a395780634f1ef28614611834578063502927d81461175257806352d1902d146116ec57806358b9b1e7146111ae578063606bf4071461115757806361d5a731146109c4578063621fa08914610985578063715018a61461093c57806379ba5097146108ee578063839692bc146108975780638da5cb5b1461086357806391ef5b51146107f1578063a43f5966146106eb578063a6946031146105dc578063ad3cb1cc14610595578063b1b805b514610473578063bf89efd21461040a578063c4d66de81461028b578063e30c397814610257578063ec8efd211461023a578063f2fde38b146101b65763f62983f01461014d575f80fd5b346101b25760203660031901126101b2576101666121e1565b5060a0610174600435612239565b5160806040519161ffff8151168352602081015115156020840152604081015115156040840152606081015115156060840152015115156080820152f35b5f80fd5b346101b25760203660031901126101b2576101cf611e40565b6101d7612346565b5f516020612be05f395f51905f5280546001600160a01b0319166001600160a01b039283169081179091555f516020612b405f395f51905f52549091167f38d16b8cac22d99fc7c124b9cd0de2d3fa1faef420bfe791d8c362d765e227005f80a3005b346101b2575f3660031901126101b2576020604051620151808152f35b346101b2575f3660031901126101b2575f516020612be05f395f51905f52546040516001600160a01b039091168152602090f35b346101b25760203660031901126101b2576102a4611e40565b5f516020612bc05f395f51905f52549060ff8260401c1615916001600160401b03811680159081610402575b60011490816103f8575b1590816103ef575b506103e05767ffffffffffffffff1981166001175f516020612bc05f395f51905f5255826103b4575b50610314612824565b61031c612824565b6001600160a01b038116156103a157610334906123ac565b61033c612824565b610344612824565b61034a57005b68ff0000000000000000195f516020612bc05f395f51905f5254165f516020612bc05f395f51905f52557fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d2602060405160018152a1005b631e4fbdf760e01b5f525f60045260245ffd5b68ffffffffffffffffff191668010000000000000001175f516020612bc05f395f51905f52558261030b565b63f92ee8a960e01b5f5260045ffd5b905015846102e2565b303b1591506102da565b8491506102d0565b346101b25760203660031901126101b2576004355f9081525f516020612b005f395f51905f5260209081526040918290205482516001600160a01b038216815260a082901c6001600160401b03169281019290925260e01c9181019190915280606081015b0390f35b346101b25760c03660031901126101b25760043560a03660231901126101b25761049b612346565b805f525f516020612ae05f395f51905f5260205260405f2060243561ffff81168091036101b25781549060443590811515918281036101b2575060643590811515918281036101b25762ff00009263ff000000915060181b169363ffffffff1916179160101b1617178155608435801515908181036101b2575064ff0000000082549160201b169064ff00000000191617815560a435908115158083036101b257815465ff0000000000191660289190911b65ff000000000016178155600101805464ff000000001916640100000000179055507fb01c0a30cea93f12bdaa4228f3ced17148be2cc93045c1848d99d588c0141c8f5f80a2005b346101b2575f3660031901126101b25761046f6040516105b6604082611f26565b60058152640352e302e360dc1b6020820152604051918291602083526020830190611e56565b346101b25760403660031901126101b25760043563ffffffff6105fd611ea7565b610605612346565b169081156106dc57805f525f516020612ae05f395f51905f5260205260ff600160405f20015460201c16156106ca575f8181525f516020612b605f395f51905f52602090815260408083208584529091529020546001600160a01b0316156106b557805f525f516020612ae05f395f51905f52602052600160405f20018263ffffffff198254161790557f39de5aa871268e261cc41a46876cebce3b7b112a7b158b360f850538860a9bab5f80a3005b6312eaaba960e21b5f5260045260245260445ffd5b6313f6864d60e01b5f5260045260245ffd5b635498399960e11b5f5260045ffd5b346101b25760403660031901126101b257600435610707611ea7565b90610710612346565b5f8181525f516020612b605f395f51905f526020908152604080832063ffffffff861684529091529020546001600160a01b0316156107d457805f525f516020612ae05f395f51905f5260205263ffffffff80600160405f2001541692168092146107bf57805f525f516020612b605f395f51905f5260205260405f20825f526020525f60408120557f2af3e10bdbb7f8ceef8368a212ed59736df2eb37120a7ef435c48d980c04480a5f80a3005b637d788d8f60e11b5f5260045260245260445ffd5b9063ffffffff916312eaaba960e21b5f526004521660245260445ffd5b346101b25761081f61081961080536611f98565b929061081083612239565b51933691611f62565b9061220b565b901561085a575f525f516020612ba05f395f51905f52602052602060018060a01b0360405f2054165b6040516001600160a01b039091168152f35b5060205f610848565b346101b2575f3660031901126101b2575f516020612b405f395f51905f52546040516001600160a01b039091168152602090f35b346101b25760403660031901126101b2576108b0611ea7565b6004355f525f516020612b605f395f51905f5260205263ffffffff60405f2091165f52602052602060405f2060018060a01b03905416604051908152f35b346101b2575f3660031901126101b2575f516020612be05f395f51905f5254336001600160a01b039091160361092957610927336123ac565b005b63118cdaa760e01b5f523360045260245ffd5b346101b2575f3660031901126101b25760405162461bcd60e51b81526020600482015260116024820152701c995b9bdd5b98d948191a5cd8589b1959607a1b6044820152606490fd5b346101b25760203660031901126101b2576004355f525f516020612ae05f395f51905f52602052602063ffffffff600160405f20015416604051908152f35b346101b25760603660031901126101b2576004356024356001600160401b0381116101b2576109f7903690600401611e7a565b9060443592831591821585036101b257610a1082612239565b9263ffffffff60208501511691835f525f516020612b605f395f51905f5260205260405f2063ffffffff84165f5260205260405f209060405191610a5383611eba565b549160018060a01b0383168082526001600160401b03602083019460a01c16845215611140575160405163473b057f60e11b81526020600482015260248101899052975f9289926001600160a01b031691839160449183918190838501378181018301879052601f01601f191681010301915afa958615611135575f9661106f575b506040860180516001600160a01b03161561106057516001600160a01b031633810361104a5750606086016001600160401b038151161561103b578651511561102c576001600160401b038151166001600160401b038351166001600160401b034216016001600160401b038111610ff7576001600160401b0316908181116110175750505190516001600160401b0390811691168181111561100b57036001600160401b038111610ff757610b9490945b602087015190519061244a565b906005811015610fe35760018114610fd45760028114610fc55760038114610fb657600414610fa757610bc88651856122c7565b95610bd382866126b7565b97875f525f516020612b005f395f51905f52602052610c026001600160401b0360405f205460a01c1688612722565b885f525f516020612ba05f395f51905f52602052610c306001600160401b0360405f205460a01c1688612722565b610cd4604051610c3f81611ef0565b3381526001600160401b03989098166020808a018281526040808c018a81525f8e81525f516020612b005f395f51905f5290945292209a518b5491516001600160e01b03199092166001600160a01b03919091161760a09190911b67ffffffffffffffff60a01b16178a5590989063ffffffff905b5182546001600160e01b0316911660e01b6001600160e01b031916179055565b610d4d604051610ce381611ef0565b33815260208082018a815260408084018a81525f8f81525f516020612ba05f395f51905f5290945292209251835491516001600160e01b03199092166001600160a01b03919091161760a09190911b67ffffffffffffffff60a01b1617825563ffffffff90610cb4565b610d5889898861274f565b875f527f064503501234cc9c6e116cf4a84c07475158dabb6a3dcee437a89227e23bf2046020528860405f2055885f527f064503501234cc9c6e116cf4a84c07475158dabb6a3dcee437a89227e23bf2056020528760405f205592610f81575b82610e17575b5f516020612b205f395f51905f5294939291610def610dfd925160405196875260c0602088015260c0870190611e56565b908582036040870152611e56565b9460608401521515608083015260a08201528033930390a4005b610e2033611fcb565b855f5260205260405f2082516001600160401b038111610f6d57610e448254612003565b601f8111610f2a575b506020601f8211600114610eb057925f516020612b205f395f51905f529796959492610e9583610def94610dfd975f91610ea5575b508160011b915f199060031b1c19161790565b90555b9250509192939450610dbe565b90508601518f610e82565b601f19821690835f52805f20915f5b818110610f12575083610dfd96935f516020612b205f395f51905f529b9a99989693610def9660019410610efa575b5050811b019055610e98565b8701515f1960f88460031b161c191690558e80610eee565b9192602060018192868b015181550194019201610ebf565b81811115610e4d57610f5f90835f5260205f2090601f840160051c9060208510610f65575b601f82910160051c0391016121c6565b8a610e4d565b5f9150610f4f565b634e487b7160e01b5f52604160045260245ffd5b9150610f8c33611fcb565b845f52602052610f9f60405f2054612003565b151591610db8565b63a9b0b66560e01b5f5260045ffd5b634bd5d0ad60e01b5f5260045ffd5b630183219160e51b5f5260045ffd5b6375ebfc4960e01b5f5260045ffd5b634e487b7160e01b5f52602160045260245ffd5b634e487b7160e01b5f52601160045260245ffd5b5050610b945f94610b87565b63182f1ac760e21b5f5260045260245260445ffd5b63eae8cfd360e01b5f5260045ffd5b634f1a06e760e11b5f5260045ffd5b634dab16af60e11b5f526004523360245260445ffd5b633c88bf6360e01b5f5260045ffd5b9095503d805f833e6110818183611f26565b8101906020818303126101b2578051906001600160401b0382116101b257016080818303126101b257604051916110b783611f0b565b81516001600160401b0381116101b257816110d3918401612366565b83526020820151906001600160401b0382116101b2576110f4918301612366565b60208301526040810151906001600160a01b03821682036101b257606091604084015201516001600160401b03811681036101b25760608201529487610ad5565b6040513d5f823e3d90fd5b84866312eaaba960e21b5f5260045260245260445ffd5b346101b25761118461117e61116b36611f98565b611176839493612239565b503691611f62565b906122c7565b5f525f516020612b005f395f51905f52602052602060018060a01b0360405f205416604051908152f35b346101b25760803660031901126101b2576004356111ca611ea7565b906044356001600160401b0381116101b2576111ea903690600401611e7a565b9160643593841590811586036101b25763ffffffff169081156106dc5761121083612239565b93835f525f516020612b605f395f51905f5260205260405f20835f5260205260405f20906040519161124183611eba565b549160018060a01b0383168082526001600160401b03602083019460a01c16845215611140575160405163473b057f60e11b81526020600482015260248101899052975f9289926001600160a01b031691839160449183918190838501378181018301879052601f01601f191681010301915afa958615611135575f96611626575b506040860180516001600160a01b03161561106057516001600160a01b031633810361104a5750606086016001600160401b038151161561103b578651511561102c576001600160401b038151166001600160401b038351166001600160401b034216016001600160401b038111610ff7576001600160401b0316908181116110175750505190516001600160401b0390811691168181111561161a57036001600160401b038111610ff7576113819094602087015190519061244a565b906005811015610fe35760018114610fd45760028114610fc55760038114610fb657600414610fa7576113b58651856122c7565b956113c082866126b7565b97875f525f516020612b005f395f51905f526020526113ef6001600160401b0360405f205460a01c1688612722565b885f525f516020612ba05f395f51905f5260205261141d6001600160401b0360405f205460a01c1688612722565b61142c604051610c3f81611ef0565b61143b604051610ce381611ef0565b61144689898861274f565b875f527f064503501234cc9c6e116cf4a84c07475158dabb6a3dcee437a89227e23bf2046020528860405f2055885f527f064503501234cc9c6e116cf4a84c07475158dabb6a3dcee437a89227e23bf2056020528760405f2055926115f4575b826114dc575f516020612b205f395f51905f5294939291610def610dfd925160405196875260c0602088015260c0870190611e56565b6114e533611fcb565b855f5260205260405f2082516001600160401b038111610f6d576115098254612003565b601f81116115ba575b506020601f821160011461155957925f516020612b205f395f51905f529796959492610e9583610def94610dfd975f91610ea557508160011b915f199060031b1c19161790565b601f19821690835f52805f20915f5b8181106115a2575083610dfd96935f516020612b205f395f51905f529b9a99989693610def9660019410610efa575050811b019055610e98565b9192602060018192868b015181550194019201611568565b81811115611512576115ee90835f5260205f2090601f840160051c9060208510610f6557601f82910160051c0391016121c6565b8a611512565b91506115ff33611fcb565b845f5260205261161260405f2054612003565b1515916114a6565b50506113815f94610b87565b9095503d805f833e6116388183611f26565b8101906020818303126101b2578051906001600160401b0382116101b257016080818303126101b2576040519161166e83611f0b565b81516001600160401b0381116101b2578161168a918401612366565b83526020820151906001600160401b0382116101b2576116ab918301612366565b60208301526040810151906001600160a01b03821682036101b257606091604084015201516001600160401b03811681036101b257606082015294876112c3565b346101b2575f3660031901126101b2577f00000000000000000000000000000000000000000000000000000000000000006001600160a01b031630036117435760206040515f516020612b805f395f51905f528152f35b63703e46dd60e11b5f5260045ffd5b346101b25760403660031901126101b25761177361176e611e40565b611fcb565b6024355f5260205260405f20604051905f9080549061179182612003565b808552916001811690811561180d57506001146117cd575b61046f846117b981860382611f26565b604051918291602083526020830190611e56565b5f90815260208120939250905b8082106117f3575090915081016020016117b9826117a9565b9192600181602092548385880101520191019092916117da565b60ff191660208087019190915292151560051b850190920192506117b991508390506117a9565b60403660031901126101b257611848611e40565b6024356001600160401b0381116101b257366023820112156101b257611878903690602481600401359101611f62565b6001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016308114908115611a17575b50611743576118ba612346565b6040516352d1902d60e01b81526001600160a01b0383169290602081600481875afa5f91816119e3575b506118fc5783634c9c8ce360e01b5f5260045260245ffd5b805f516020612b805f395f51905f528592036119d15750813b156119bf575f516020612b805f395f51905f5280546001600160a01b031916821790557fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b5f80a28151156119a7575f8083602061092795519101845af43d1561199f573d9161198383611f47565b926119916040519485611f26565b83523d5f602085013e612a81565b606091612a81565b5050346119b057005b63b398979f60e01b5f5260045ffd5b634c9c8ce360e01b5f5260045260245ffd5b632a87526960e21b5f5260045260245ffd5b9091506020813d602011611a0f575b816119ff60209383611f26565b810103126101b2575190856118e4565b3d91506119f2565b5f516020612b805f395f51905f52546001600160a01b031614159050836118ad565b346101b25760203660031901126101b2576004355f9081525f516020612ba05f395f51905f5260209081526040918290205482516001600160a01b03821681526001600160401b0360a083901c169281019290925260e01c91810191909152606090f35b346101b25760203660031901126101b257600435611aba33611fcb565b815f5260205260405f20611ace8154612003565b9081611afd575b82337f8e43ba98be6948052c3368d53fef5505611df0ae5994d278a646eb9ad3479edd5f80a3005b81601f5f9311600114611b145750555b8180611ad5565b81835260208320611b3191601f0160051c841901906001016121c6565b8082528160208120915555611b0d565b346101b2575f3660031901126101b257602060405160018152f35b346101b25760803660031901126101b257600435611b78611ea7565b6044356001600160a01b0381169291908390036101b257606435906001600160401b0382168092036101b25763ffffffff90611bb2612346565b169283156106dc578015611d0557620151808211611ceb57825f525f516020612ae05f395f51905f52602052600160405f20019160ff835460201c1615611cd8577f13b9f8bb5ee05a42b59cc1d4197d78c5ca07a7236ce1bd191161faca7e5bb398604086938693611c8b8351611c2881611eba565b83815260208082018481525f8981525f516020612b605f395f51905f5283528781208b8252909252908690209151825491516001600160e01b03199092166001600160a01b03919091161760a09190911b67ffffffffffffffff60a01b16179055565b82519182526020820152a363ffffffff81541615611ca557005b805463ffffffff1916831790557f39de5aa871268e261cc41a46876cebce3b7b112a7b158b360f850538860a9bab5f80a3005b836313f6864d60e01b5f5260045260245ffd5b5063911d6f1360e01b5f526004526201518060245260445ffd5b6379e2e00360e01b5f5260045ffd5b346101b25760603660031901126101b2576004356024356001600160401b0381116101b257611d47903690600401611e7a565b91604435906001600160401b0382116101b25761117e611d8d93611d93604096611d78611dc4963690600401611e7a565b979093611d8487612239565b51923691611f62565b8561220b565b9015611e10575f9081525f516020612ba05f395f51905f52602052869020546001600160a01b0316945b3691611f62565b5f9081525f516020612b005f395f51905f52602052829020546001600160a01b03918216918215159183911682611e06575b5050825191825215156020820152f35b1490508184611df6565b505f94611dbd565b346101b25760403660031901126101b25761046f6117b9611e37611e40565b60243590612088565b600435906001600160a01b03821682036101b257565b805180835260209291819084018484015e5f828201840152601f01601f1916010190565b9181601f840112156101b2578235916001600160401b0383116101b257602083818601950101116101b257565b6024359063ffffffff821682036101b257565b604081019081106001600160401b03821117610f6d57604052565b60a081019081106001600160401b03821117610f6d57604052565b606081019081106001600160401b03821117610f6d57604052565b608081019081106001600160401b03821117610f6d57604052565b90601f801991011681019081106001600160401b03821117610f6d57604052565b6001600160401b038111610f6d57601f01601f191660200190565b929192611f6e82611f47565b91611f7c6040519384611f26565b8294818452818301116101b2578281602093845f960137010152565b9060406003198301126101b25760043591602435906001600160401b0382116101b257611fc791600401611e7a565b9091565b6001600160a01b03165f9081527f064503501234cc9c6e116cf4a84c07475158dabb6a3dcee437a89227e23bf2026020526040902090565b90600182811c92168015612031575b602083101461201d57565b634e487b7160e01b5f52602260045260245ffd5b91607f1691612012565b9060405161204881611ed5565b608060ff82945461ffff81168452818160101c1615156020850152818160181c1615156040850152818160201c161515606085015260281c161515910152565b61209181611fcb565b825f5260205260405f209160405192835f8254926120ae84612003565b80845293600181169081156121a45750600114612160575b506120d392500384611f26565b82511561214f5780612105915f525f516020612ae05f395f51905f52602052836120ff60405f2061203b565b9161220b565b901561214f575f9081525f516020612ba05f395f51905f5260205260409020546001600160a01b0390811691160361213a5790565b50604051612149602082611f26565b5f815290565b505050604051612149602082611f26565b90505f9291925260205f20905f915b8183106121885750509060206120d3928201015f6120c6565b6020919350806001915483858a0101520191019091859261216f565b9050602092506120d394915060ff191682840152151560051b8201015f6120c6565b5f5b8281106121d457505050565b5f828201556001016121c8565b604051906121ee82611ed5565b5f6080838281528260208201528260408201528260608201520152565b916122159161244a565b906005811015610fe3576122315761222c916126b7565b600191565b50505f905f90565b905f6040805161224881611ef0565b6122506121e1565b81528260208201520152815f525f516020612ae05f395f51905f5260205260405f209160016040519361228285611ef0565b61228b8161203b565b855201549260ff63ffffffff85169485602084015260201c16159384159182604082015294916122be575b506106ca5750565b9050155f6122b6565b612340601960206040516122dc604082611f26565b828152017f6479616b612e6964656e746974792e69642d6e6f64652e7631000000000000008152209260208151910120916123326040519384926020840196876040919493926060820195825260208201520152565b03601f198101835282611f26565b51902090565b5f516020612b405f395f51905f52546001600160a01b0316330361092957565b81601f820112156101b25780519061237d82611f47565b9261238b6040519485611f26565b828452602083830101116101b257815f9260208093018386015e8301015290565b5f516020612be05f395f51905f5280546001600160a01b03199081169091555f516020612b405f395f51905f5280549182166001600160a01b0393841690811790915591167f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e05f80a3565b908151811015612428570160200190565b634e487b7160e01b5f52603260045260245ffd5b5f198114610ff75760010190565b80519290915f5b84811080612696575b1561246d576124689061243c565b612451565b92935b83811180612668575b1561248c578015610ff7575f1901612470565b90919293602083015115158061265f575b8061263e575b61262e575b84820391808311610ff75785146126185761ffff8351168211612602576124ce82611f47565b946124dc6040519687611f26565b828652601f196124eb84611f47565b013660208801375f5b83811061255c5750505050604081015115612530575061251382612a17565b1561251e575b5f9190565b60405160049250612149602082611f26565b6080015161253d575f9190565b6125468261294e565b6125195760405160049250612149602082611f26565b808201808311610ff7576125709084612417565b516001600160f81b0319811690604160f81b821015806125f4575b6125d1575b5061259b868261284f565b156125b757906001915f1a6125b0828a612417565b53016124f4565b505050505050905060039060206121496040519182611f26565b6020915060f81c0160ff8111610ff75760f81b6001600160f81b0319165f612590565b50602d60f91b82111561258b565b5050509050600290604051612149602082611f26565b5050509050600190604051612149602082611f26565b936126389061243c565b936124a8565b50600160fe1b6001600160f81b03196126578784612417565b5116146124a3565b5084821161249d565b505f198101818111610ff757600160fd1b906001600160f81b03199061268e9088612417565b511614612479565b50600160fd1b6001600160f81b03196126af8387612417565b51161461245a565b612340601d60206040516126cc604082611f26565b828152017f6479616b612e6964656e746974792e68616e646c652d6e6f64652e76310000008152209260208151910120916123326040519384926020840196876040919493926060820195825260208201520152565b6001600160401b0391821691168181111561273b575050565b6277f1a160e81b5f5260045260245260445ffd5b9190805f527f064503501234cc9c6e116cf4a84c07475158dabb6a3dcee437a89227e23bf20460205260405f205491821590811561281a575b5061281557815f527f064503501234cc9c6e116cf4a84c07475158dabb6a3dcee437a89227e23bf20560205260405f205403612811575f8181525f516020612ba05f395f51905f5260205260408120805467ffffffffffffffff60a01b1690553392907f278e2122d24782b703f05416ac53a9750d67bcf6fa1603b94ec95660821a89149080a4565b5050565b505050565b905082145f612788565b60ff5f516020612bc05f395f51905f525460401c161561284057565b631afcd79f60e31b5f5260045ffd5b6001600160f81b03191690606160f81b82101580612940575b6128c157600360fc1b82101580612932575b6128c15760408101516128d55760608101511515806128c8575b6128c15760800151151590816128b3575b506128ae575f90565b600190565b602d60f81b1490505f6128a5565b5050600190565b50605f60f81b8214612894565b50601760f91b8114908115612924575b8115612916575b8115612908575b81156128fd575090565b600160fe1b14919050565b605f60f81b811491506128f3565b602d60f81b811491506128ec565b602b60f81b811491506128e5565b50603960f81b82111561287a565b50603d60f91b821115612868565b8051156124285760208101516001600160f81b031916602d60f81b1480156129e7575b6129e25760015b81518110156128c157602d60f81b6001600160f81b03196129998385612417565b511614806129b4575b6129ae57600101612978565b50505f90565b505f198101818111610ff757602d60f81b906001600160f81b0319906129da9085612417565b5116146129a2565b505f90565b5080515f198101908111610ff757602d60f81b906001600160f81b031990612a0f9084612417565b511614612971565b5f195f5b8251811015612a5857600160fe1b6001600160f81b0319612a3c8386612417565b511614612a4c575b600101612a1b565b90196129ae5780612a44565b505f1981146129ae578015159182612a6f57505090565b515f19810192508211610ff757141590565b90612aa55750805115612a9657805190602001fd5b63d6bda27560e01b5f5260045ffd5b81511580612ad6575b612ab6575090565b639996b31560e01b5f9081526001600160a01b0391909116600452602490fd5b50803b15612aae56fe064503501234cc9c6e116cf4a84c07475158dabb6a3dcee437a89227e23bf203064503501234cc9c6e116cf4a84c07475158dabb6a3dcee437a89227e23bf2008acbd4495dbb2ff215c4799cd4551da5688633536a4c5381ff06eaca80119a2e9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300064503501234cc9c6e116cf4a84c07475158dabb6a3dcee437a89227e23bf206360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc064503501234cc9c6e116cf4a84c07475158dabb6a3dcee437a89227e23bf201f0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00237e158222e3e6968b72b9db0d8043aacf074ad9f650f0d1606b4d82ee432c00f0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00" }, "methodIdentifiers": { "INITIAL_VERSION()": "38e09e78", @@ -24,6 +24,7 @@ "resolvePair(bytes32,string,string)": "284e6abf", "retireVerifier(bytes32,uint32)": "a43f5966", "reverseOf(address,bytes32)": "502927d8", + "rulesOf(bytes32)": "f62983f0", "setLatestVersion(bytes32,uint32)": "a6946031", "setPlatform(bytes32,(uint16,bool,bool,bool,bool))": "b1b805b5", "setVerifier(bytes32,uint32,address,uint64)": "2de00c01", diff --git a/rust/contracts/src/artifacts.rs b/rust/contracts/src/artifacts.rs index 8a2e052..6e818da 100644 --- a/rust/contracts/src/artifacts.rs +++ b/rust/contracts/src/artifacts.rs @@ -67,6 +67,8 @@ pub const COVERED: &[(&str, &str)] = &[ ("GoogleIdentityVerifier", "GoogleIdentityVerifier"), ("XIdentityVerifier", "XIdentityVerifier"), ("IdentityJwksRoots", "IdentityJwksRoots"), + // escrow — value held against a handle nobody has claimed yet + ("HandleEscrow", "HandleEscrow"), // factory ("LibidFactory", "LibidFactory"), ]; diff --git a/rust/contracts/src/bindings/escrow.rs b/rust/contracts/src/bindings/escrow.rs new file mode 100644 index 0000000..6098db6 --- /dev/null +++ b/rust/contracts/src/bindings/escrow.rs @@ -0,0 +1,90 @@ +//! Bindings for the handle escrow (`solidity/contracts/escrow/`). +//! +//! Value held against a platform handle rather than against an account id, so +//! a sender who knows only a name can pay it before anybody has claimed it. +//! Whoever proves that handle to the naming system takes what is held. +//! +//! The escrow keys on a rules-independent form of the text, so the slot never +//! moves when a platform's normalization is reconfigured, and validates the +//! text against the platform's current rules on the way in, so nothing funds a +//! slot no proof could claim. `slotOf` is `pure` and public for exactly that +//! reason: a client computes the same key off chain. + +/// Bindings for `escrow/HandleEscrow.sol`. +#[allow(clippy::too_many_arguments, unused_attributes)] +mod escrow_inner { + use alloy::sol; + + sol! { + #[sol(rpc)] + interface HandleEscrow { + function initialize(address owner_, address names_) external; + + /// Put `amount` of `token` against a handle. `address(0)` is the + /// chain's own token, and then `amount` must equal the value sent. + /// + /// A handle that already resolves is paid STRAIGHT THROUGH to its + /// holder — the escrow is for the window before a handle is + /// claimed. Only an unclaimed handle escrows, and then there is no + /// way to take it back. Watch `Deposited` against `Forwarded` to + /// tell the two apart. + function deposit( + bytes32 platformId, + string calldata handle, + address token, + uint256 amount + ) external payable; + + /// Take everything held for a handle in one token. The caller has + /// to be the wallet that handle currently resolves to. + function claim( + bytes32 platformId, + string calldata handle, + address token, + address recipient + ) external; + + function escrowed(bytes32 platformId, string calldata handle, address token) + external + view + returns (uint256); + function escrowedAt(bytes32 slot, address token) external view returns (uint256); + function slotOf(bytes32 platformId, string memory handle) external pure returns (bytes32); + function canonicalHandle(string memory handle) external pure returns (string memory); + function names() external view returns (address); + function NATIVE() external view returns (address); + + /// `handle` rides along as text: a slot cannot be turned back into + /// a string, so an indexer reads the name from here. + event Deposited( + bytes32 indexed slot, + address indexed token, + address indexed depositor, + bytes32 platformId, + string handle, + uint256 amount + ); + /// A deposit for a handle somebody already held, paid to that holder + /// and never booked. Nothing is claimable afterwards, which is what + /// separates it from `Deposited`. + event Forwarded( + bytes32 indexed slot, + address indexed token, + address indexed depositor, + address holder, + bytes32 platformId, + string handle, + uint256 amount + ); + event Claimed( + bytes32 indexed slot, + address indexed token, + address indexed claimer, + address recipient, + uint256 amount + ); + } + } +} + +pub use escrow_inner::HandleEscrow; diff --git a/rust/contracts/src/bindings/mod.rs b/rust/contracts/src/bindings/mod.rs index f7a82ba..dd79f0e 100644 --- a/rust/contracts/src/bindings/mod.rs +++ b/rust/contracts/src/bindings/mod.rs @@ -1,6 +1,7 @@ //! Hand-written `alloy::sol!` bindings, kept in lockstep with the Solidity //! sources in `solidity/contracts`. Grouped by product area. +pub mod escrow; pub mod factory; pub mod identity; pub mod login; diff --git a/rust/contracts/tests/anvil.rs b/rust/contracts/tests/anvil.rs index 90f6972..0cd75fe 100644 --- a/rust/contracts/tests/anvil.rs +++ b/rust/contracts/tests/anvil.rs @@ -11,9 +11,11 @@ use alloy::{ Provider, ProviderBuilder, }, + sol_types::SolValue, }; use libid_contracts::{ bindings::{ + escrow::HandleEscrow, identity::{ GitHubIdentityVerifier, IdentityNames, @@ -298,6 +300,167 @@ async fn deploys_the_identity_stack() { ); } +/// (c2) The handle escrow against a real chain: deploy it, pay a handle nobody +/// has claimed, and watch two spellings land in one slot. +/// +/// The payout path is covered by the Solidity suite, which stages proofs +/// through a mock verifier. That mock deliberately does NOT ship in this +/// crate's artifacts: it reports whatever a caller stages, so a copy reachable +/// from a deploy tool is a way to mint any identity on a live chain. What is +/// left for Rust is what Rust owns — the artifact deploys, the binding shapes, +/// and the slot derivation agreeing with the contract. +#[tokio::test] +async fn escrows_value_against_an_unclaimed_handle() { + let provider = test_provider(); + let artifacts = Artifacts::embedded(); + let deployer = default_signer(&provider).await; + let stranger = provider.get_accounts().await.unwrap()[1]; + + let names_proxy = deploy_behind_proxy( + &provider, + &artifacts, + "IdentityNames", + &IdentityNames::initializeCall { owner_: deployer }, + None, + ) + .await + .unwrap(); + + let notary_proxy = deploy_behind_proxy( + &provider, + &artifacts, + "Notary", + &Notary::initializeCall { + owner_: deployer, + notary_: Address::repeat_byte(0x11), + }, + None, + ) + .await + .unwrap(); + + // A real verifier, so the platform is wired the way a deployment wires it. + // Nothing here calls it: the escrow only reads `rulesOf`, which needs the + // platform to have one. + let github_proxy = deploy_behind_proxy( + &provider, + &artifacts, + "GitHubIdentityVerifier", + &GitHubIdentityVerifier::initializeCall { + owner_: deployer, + notaryContract_: notary_proxy, + shape_: GitHubIdentityVerifier::ResponseShape { + endpoint: "/user".into(), + handlePrefix: "\"login\":\"".into(), + idPrefix: "\"id\":".into(), + idSuffix: ",".into(), + }, + }, + None, + ) + .await + .unwrap(); + + let platform_id = keccak256(b"dyaka.identity.platform.github"); + let names = IdentityNames::new(names_proxy, &provider); + names + .setPlatform( + platform_id, + IdentityNames::Rules { + maxLength: 39, + stripLeadingAt: true, + isEmail: false, + allowUnderscore: false, + allowHyphen: true, + }, + ) + .send() + .await + .unwrap() + .get_receipt() + .await + .unwrap(); + names + .setVerifier(platform_id, 1, github_proxy, 300) + .send() + .await + .unwrap() + .get_receipt() + .await + .unwrap(); + + let escrow_proxy = deploy_behind_proxy( + &provider, + &artifacts, + "HandleEscrow", + &HandleEscrow::initializeCall { + owner_: deployer, + names_: names_proxy, + }, + None, + ) + .await + .unwrap(); + let escrow = HandleEscrow::new(escrow_proxy, &provider); + assert_eq!(escrow.names().call().await.unwrap(), names_proxy); + + // The slot a client computes off chain has to be the slot the contract + // keys on, or an indexer watches the wrong one. + let slot_v1 = keccak256(b"libid.escrow.handle-slot.v1"); + let computed = + keccak256((slot_v1, platform_id, keccak256(b"alice-1")).abi_encode_params()); + assert_eq!( + escrow + .slotOf(platform_id, " Alice-1 ".into()) + .call() + .await + .unwrap(), + computed, + "Rust and the contract derive different slots" + ); + + // Two spellings of one handle, paid before anybody holds it. + let amount = U256::from(1_000_000_000_000_000_000u64); + for spelling in [" Alice-1 ", "alice-1"] { + escrow + .deposit(platform_id, spelling.into(), Address::ZERO, amount) + .value(amount) + .send() + .await + .unwrap() + .get_receipt() + .await + .unwrap(); + } + + assert_eq!( + escrow + .escrowed(platform_id, "ALICE-1".into(), Address::ZERO) + .call() + .await + .unwrap(), + amount * U256::from(2), + "the two spellings did not accumulate in one slot" + ); + + // Nobody holds the handle, so nobody can take it — including the depositor. + let claim = escrow + .claim(platform_id, "alice-1".into(), Address::ZERO, stranger) + .from(stranger) + .send() + .await; + // The AUTHORIZATION refusal specifically. A bare `is_err` would also pass + // on a mistyped platform, an unwired one or an RPC hiccup, so it would + // stay green with the holder check removed entirely. + let err = claim + .expect_err("an unheld handle was claimable") + .to_string(); + assert!( + err.contains("NotTheHolder") || err.contains("0xb6bd8e83"), + "refused for the wrong reason: {err}" + ); +} + /// (d) XHonkVerifier via linked bytecode — exercises the recursive library /// deploy + link-reference substitution (ZKTranscriptLib). #[tokio::test] diff --git a/scripts/vendor-artifacts.sh b/scripts/vendor-artifacts.sh index ec8d921..467665c 100755 --- a/scripts/vendor-artifacts.sh +++ b/scripts/vendor-artifacts.sh @@ -55,6 +55,8 @@ ARTIFACTS=( "GoogleIdentityVerifier:GoogleIdentityVerifier" "XIdentityVerifier:XIdentityVerifier" "IdentityJwksRoots:IdentityJwksRoots" + # escrow — value held against a handle nobody has claimed yet + "HandleEscrow:HandleEscrow" # factory "LibidFactory:LibidFactory" ) diff --git a/solidity/contracts/escrow/HandleEscrow.sol b/solidity/contracts/escrow/HandleEscrow.sol new file mode 100644 index 0000000..dd827d8 --- /dev/null +++ b/solidity/contracts/escrow/HandleEscrow.sol @@ -0,0 +1,457 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import {Ownable2StepUpgradeable} from "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol"; +import { + ReentrancyGuardTransientUpgradeable +} from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardTransientUpgradeable.sol"; + +import {HandleNormalizer} from "../identity/HandleNormalizer.sol"; +import {IIdentityNames} from "./IIdentityNames.sol"; + +/// @title HandleEscrow - send to a platform handle before anybody claims it. +/// +/// @notice Holds tokens against a platform handle. Whoever proves that handle +/// to the naming system takes what is held for it. +/// +/// @dev The point is a sender who knows `@alice` and nothing else. The Bank's +/// escrow is keyed by an account's immutable id, which such a sender does +/// not have, so this keys by the handle itself. +/// +/// **The handle is the whole key. There is no deadline and no refund.** +/// That was decided deliberately, and the consequence is stated here +/// rather than left to be discovered: A PLATFORM THAT RECYCLES A HANDLE +/// HANDS THE NEW HOLDER WHATEVER ACCUMULATED FOR THE PREVIOUS ONE. So does +/// a rename — the account that renames away stops being able to claim, and +/// whoever proves the freed handle next receives it. A depositor cannot +/// take a deposit back, and neither can the owner. Send to a handle the +/// way you send to an address: because you mean that name to have it. +/// +/// **The escrow is for the window before a handle is claimed, and only +/// that.** A deposit for a handle that already resolves is paid straight to +/// its holder; once an account has claimed its identity, holding the value +/// would add a claim transaction and reach the same wallet. So an escrow +/// slot exists only while nobody holds the handle, and stops being written +/// the moment somebody does. +/// +/// **What the escrow keys on is NOT what it validates against.** The slot +/// comes from a rules-independent form of the text — see `_canonical` — so +/// the owner reconfiguring a platform's normalization cannot move money +/// that is already held. The text is separately checked against the +/// platform's CURRENT rules on the way in, so nobody funds a slot that +/// could never be claimed. A later rules change therefore decides what is +/// accepted next, and never where what is already here belongs. +/// +/// What it can still do is STRAND. A claim is authorized by +/// `resolveHandle`, which normalizes under the rules of the moment, so an +/// owner who narrows a platform — dropping the underscore, say — makes +/// handles carrying one resolve to nobody, and the value keyed to them +/// waits, at the same key, until compatible rules return. Immunity from +/// re-keying is not immunity from that. +/// +/// **The transform is pinned here, the rules are read live.** This +/// contract carries its own compiled copy of `HandleNormalizer` and reads +/// only `Rules` from the naming system. That is what makes the key +/// independent of configuration — but it also means the naming system +/// cannot change the TRANSFORM alone: it is separately upgradeable, and a +/// normalizer that folded differently there would break the agreement +/// silently, in the direction of one identity holding two slots. Such an +/// upgrade has to carry an upgrade here. The same goes for a `Rules` +/// field appended on that side: this contract's decoder would drop it. +/// +/// **The naming contract is set once and never moved.** Repointing it +/// would redirect every entitlement held, so changing it is an upgrade, +/// which leaves a record. +/// +/// **There is no pause.** A pause on `claim` freezes other people's money +/// behind an owner key, and the emergency lever is the upgrade, which is +/// already visible. +/// +/// **The naming system's owner is part of this contract's trust base, and +/// that is not a pause substitute — it is larger.** Authorization here is +/// `resolveHandle` and nothing else, so whoever can decide what that +/// answers can take what is held. The naming owner can: `setVerifier` +/// installs a verifier it controls, a `bind` through it makes it the +/// holder of any handle, and `claim` then pays it. Two ordinary +/// transactions, no upgrade and no proxy event. Read every guarantee here +/// as "under an honest naming owner"; it is the same key that already +/// decides which proofs mint names at all, so this adds no new party — +/// only a new thing that key can reach. +contract HandleEscrow is Initializable, UUPSUpgradeable, Ownable2StepUpgradeable, ReentrancyGuardTransientUpgradeable { + using SafeERC20 for IERC20; + + /// @notice The native token of the chain, as a token address. + address public constant NATIVE = address(0); + + /// @custom:storage-location erc7201:libid.storage.HandleEscrow + struct HandleEscrowStorage { + /// slot -> token -> amount held. + mapping(bytes32 => mapping(address => uint256)) held; + /// The naming system this escrow resolves through. Set in `initialize`. + IIdentityNames names; + } + + // keccak256(abi.encode(uint256(keccak256("libid.storage.HandleEscrow")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 private constant HANDLE_ESCROW_STORAGE = 0xfcca8d7d2c66f78c2760f3fcd99e0bf938b0aeb0d0b471f481dd50b8aff6b400; + + /// @dev One namespaced root, as `IdentityNames` and `Registry` have. Fields + /// may be APPENDED on upgrade; reordering or removing one would make + /// every stored balance read out of the wrong bytes, and a balance read + /// from the wrong bytes does not revert — it answers. + function _s() private pure returns (HandleEscrowStorage storage $) { + assembly { + $.slot := HANDLE_ESCROW_STORAGE + } + } + + /// @dev The domain separator of the slot key. Version it rather than + /// changing the derivation in place: a new scheme must land on new + /// slots, not silently re-point the funded ones. + bytes32 private constant HANDLE_SLOT_V1 = keccak256(bytes("libid.escrow.handle-slot.v1")); + + // ─── Events ───────────────────────────────────────────────────── + + /// @notice Value was placed against a handle. + /// @dev `handle` rides along as text so an indexer can show who it is for + /// without turning a slot back into a string, which cannot be done. + event Deposited( + bytes32 indexed slot, + address indexed token, + address indexed depositor, + bytes32 platformId, + string handle, + uint256 amount + ); + + /// @notice A deposit for a handle somebody already held was paid straight to + /// that holder, and never entered the books. + /// @dev Distinct from `Deposited` on purpose: an indexer must be able to tell + /// "this is waiting" from "this was delivered", and no balance changed + /// here for it to read. + event Forwarded( + bytes32 indexed slot, + address indexed token, + address indexed depositor, + address holder, + bytes32 platformId, + string handle, + uint256 amount + ); + + /// @notice The holder of a handle took what was held for it. + event Claimed( + bytes32 indexed slot, address indexed token, address indexed claimer, address recipient, uint256 amount + ); + + // ─── Errors ───────────────────────────────────────────────────── + + /// A deposit of nothing writes nothing. + error ZeroAmount(); + /// Native value must equal the amount, and a token deposit carries none. + error ValueMismatch(uint256 expected, uint256 provided); + /// Nothing is held for this handle in this token. + error NothingHeld(bytes32 slot, address token); + /// The caller does not hold this handle. + error NotTheHolder(address holder, address caller); + /// A payout to the zero address burns it; one to this contract strands it. + error BadRecipient(address recipient); + /// Text this platform could never accept as a handle. + error UnusableHandle(HandleNormalizer.Problem problem); + /// The recipient refused the transfer. + error NativeTransferFailed(address recipient, uint256 amount); + /// The escrow needs a naming system to resolve through. + error NoNames(); + + // ─── Setup ────────────────────────────────────────────────────── + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + function initialize(address owner_, IIdentityNames names_) external initializer { + if (address(names_) == address(0)) revert NoNames(); + __Ownable_init(owner_); + __Ownable2Step_init(); + __UUPSUpgradeable_init(); + __ReentrancyGuardTransient_init(); + _s().names = names_; + } + + /// @notice The naming system this escrow resolves through. + function names() external view returns (IIdentityNames) { + return _s().names; + } + + // ─── Depositing ───────────────────────────────────────────────── + + /// @notice Put `amount` of `token` against a handle. + /// + /// @dev Escrows whether or not somebody holds the handle today. The + /// depositor gives up the value: there is no way to take it back. + /// + /// The text is validated against the platform's current rules, so a + /// typo — a space, a character the platform forbids, a handle past its + /// length — is refused here rather than accepted into a slot no proof + /// could ever claim. + /// + /// **A handle somebody already holds is paid straight through.** The + /// escrow exists for the window before a handle is claimed. Once it is + /// claimed the escrow has no purpose: holding the value would only add + /// a claim transaction to reach the same wallet. So a deposit for a + /// resolving handle is a payment, and only an unclaimed handle escrows. + /// + /// This is what makes a deposit depend on the recipient: a holder that + /// cannot receive value fails the whole call. That is the honest + /// outcome — the sender learns instead of the value waiting in a slot + /// only that same wallet could ever claim. + /// + /// **The race is accepted.** One calldata has two outcomes depending on + /// whether it lands before or after a `bind` in the same block: it pays + /// through, or it escrows and waits for a claim. Both deliver to the + /// holder of the handle, so the difference is one transaction, not one + /// of destination. + /// + /// When it does escrow, what is credited is the balance the contract + /// actually gained, not the amount asked for, so a token that takes a + /// fee on transfer cannot make the books promise more than the contract + /// holds. A payment through is not measured, because nothing is booked: + /// such a token simply delivers less, exactly as it would on a direct + /// transfer. + /// + /// @param platformId Which platform the handle belongs to. + /// @param handle The handle, as written. Case and surrounding spaces do + /// not matter; a leading at-sign is refused, because it + /// is the same handle spelled another way. + /// @param token The ERC-20, or `NATIVE` for the chain's own token. + /// @param amount How much. For `NATIVE` it must equal `msg.value`. + function deposit(bytes32 platformId, string calldata handle, address token, uint256 amount) + external + payable + nonReentrant + { + if (amount == 0) revert ZeroAmount(); + bytes32 slot = _requireUsableSlot(platformId, handle); + + if (token == NATIVE) { + if (msg.value != amount) revert ValueMismatch(amount, msg.value); + } else if (msg.value != 0) { + // Ether riding on a token deposit has nowhere to land. + revert ValueMismatch(0, msg.value); + } + + address holder = _s().names.resolveHandle(platformId, handle); + if (holder != address(0)) { + uint256 delivered = amount; + if (token == NATIVE) { + _sendNative(holder, amount); + } else { + // What the holder GAINED, not what was asked for. A token that + // takes a fee on transfer delivers less, and an event carrying + // the requested figure would be the only record of a payment + // that never happened at that size — the escrowed path at + // least has a stored balance to correct against. + uint256 before = IERC20(token).balanceOf(holder); + IERC20(token).safeTransferFrom(msg.sender, holder, amount); + delivered = IERC20(token).balanceOf(holder) - before; + } + emit Forwarded(slot, token, msg.sender, holder, platformId, handle, delivered); + return; + } + + uint256 credited; + if (token == NATIVE) { + credited = amount; + } else { + uint256 before = IERC20(token).balanceOf(address(this)); + IERC20(token).safeTransferFrom(msg.sender, address(this), amount); + credited = IERC20(token).balanceOf(address(this)) - before; + if (credited == 0) revert ZeroAmount(); + } + + _s().held[slot][token] += credited; + emit Deposited(slot, token, msg.sender, platformId, handle, credited); + } + + // ─── Claiming ─────────────────────────────────────────────────── + + /// @notice Take everything held for a handle in one token. + /// + /// @dev Authorized by the naming system and nothing else: the caller has to + /// be the wallet that handle currently resolves to. Copying this + /// calldata out of the mempool gains nothing, because the check is + /// against the caller. + /// + /// A retired handle — one whose account renamed away — resolves to + /// nobody, so its slot waits until somebody proves that handle again. + /// That may be a different account, and then the balance is theirs. + /// See the contract comment. + /// + /// **The destination is checked, not only the caller.** The zero + /// address accepts a native transfer without reverting, so an unset + /// recipient would burn the slot and log a success; this contract's + /// own address would empty the books while the value stayed put as + /// surplus nothing points at. Neither is recoverable — there is no + /// refund and no owner lever — so both are refused here. + /// + /// The platform's rules are NOT re-checked on the way out. Text that + /// no longer normalizes resolves to nobody, so the holder check + /// already refuses it, and re-checking would only replace an honest + /// `NotTheHolder` with a misleading `UnusableHandle`. + /// + /// @param recipient Where the value goes. The claimer's choice, so a wallet + /// that holds the name can pay out somewhere else. + function claim(bytes32 platformId, string calldata handle, address token, address recipient) external nonReentrant { + if (recipient == address(0) || recipient == address(this)) revert BadRecipient(recipient); + + bytes32 slot = slotOf(platformId, handle); + + address holder = _s().names.resolveHandle(platformId, handle); + if (holder != msg.sender) revert NotTheHolder(holder, msg.sender); + + uint256 amount = _s().held[slot][token]; + if (amount == 0) revert NothingHeld(slot, token); + _s().held[slot][token] = 0; + + if (token == NATIVE) { + _sendNative(recipient, amount); + } else { + IERC20(token).safeTransfer(recipient, amount); + } + emit Claimed(slot, token, msg.sender, recipient, amount); + } + + // ─── Reading ──────────────────────────────────────────────────── + + /// @notice How much is held for a handle in one token. + function escrowed(bytes32 platformId, string calldata handle, address token) external view returns (uint256) { + return _s().held[slotOf(platformId, handle)][token]; + } + + /// @notice The same, for a slot already computed. + /// + /// @dev Takes the slot as given and checks nothing about it, which is what + /// an indexer reading raw slots off the logs needs. A slot that was + /// never funded and a slot that could never exist both answer zero, + /// and this cannot tell them apart. Where the handle is known, use + /// `escrowed` — but note it applies only the refusals that KEYING + /// needs, not the platform's rules; text a deposit would refuse can + /// still be read for. + function escrowedAt(bytes32 slot, address token) external view returns (uint256) { + return _s().held[slot][token]; + } + + /// @notice The slot a handle keys to. + /// + /// @dev Public so an indexer, a Rust deployer and a browser can all reach + /// the same key, and so a caller can check two spellings land together + /// before it sends anything. + function slotOf(bytes32 platformId, string memory handle) public pure returns (bytes32) { + return keccak256(abi.encode(HANDLE_SLOT_V1, platformId, keccak256(bytes(_canonical(handle))))); + } + + /// @notice The form of a handle this contract keys on. + /// @dev Exposed so the transform can be checked against the naming system's + /// own by anybody, in any language. + function canonicalHandle(string memory handle) public pure returns (string memory) { + return _canonical(handle); + } + + function _sendNative(address to, uint256 amount) private { + (bool ok,) = to.call{value: amount}(""); + if (!ok) revert NativeTransferFailed(to, amount); + } + + // ─── The key ──────────────────────────────────────────────────── + + /// @dev The form a handle keys on, WITHOUT reading a platform's rules. + /// + /// Rules-independent on purpose. Deriving the slot from + /// `HandleNormalizer.normalize` would tie every held balance to the + /// configuration of the moment, and the owner narrowing a platform's + /// rules would move money already deposited onto keys nobody asks + /// about. What varies by platform belongs in the check on the way in, + /// not in the key. + /// + /// It is a PREFIX of the naming system's transform: trim ASCII spaces, + /// fold A-Z. Both steps are also the first steps of `tryNormalize`, + /// neither touches a byte the other cares about, and both are + /// idempotent — so for text the platform accepts, this returns exactly + /// what `normalize` returns, whatever the rules are. + /// + /// The one place the two could disagree is `stripLeadingAt`, which + /// folds an at-prefixed spelling and a bare one together in the naming + /// system. So this drops one leading at-sign too, and the two spellings + /// reach one slot instead of splitting one identity's money across two + /// keys with half of it unreachable. + function _canonical(string memory handle) private pure returns (string memory) { + bytes memory input = bytes(handle); + + uint256 start = 0; + uint256 end = input.length; + while (start < end && input[start] == 0x20) { + start++; + } + while (end > start && input[end - 1] == 0x20) { + end--; + } + + // Drop ONE leading at-sign, exactly as `stripLeadingAt` does. Doing it + // unconditionally is safe because no expressible `Rules` accepts a + // handle whose text starts with one: a non-email platform refuses + // `0x40` outright, and an email needs a name part before its `@`. So + // for a platform that strips, both spellings reach one slot and agree + // with the resolver; for one that does not, the spelling with the + // at-sign is refused by the rules check and never funds anything. + // + // The invariant to keep: a platform must never accept a handle + // beginning with an at-sign. If one ever could, `@x` and `x` would be + // two identities sharing a slot. + if (start < end && input[start] == 0x40) { + start++; + } + + uint256 length = end - start; + if (length == 0) revert UnusableHandle(HandleNormalizer.Problem.Empty); + + bytes memory out = new bytes(length); + for (uint256 i = 0; i < length; i++) { + bytes1 c = input[start + i]; + if (c >= 0x41 && c <= 0x5A) { + c = bytes1(uint8(c) + 0x20); + } + out[i] = c; + } + return string(out); + } + + /// @dev The slot for text the platform can actually accept. + /// + /// Both halves matter. `rulesOf` reverts for a platform that is not + /// wired, which catches a mistyped `platformId` before it takes + /// anybody's money. `tryNormalize` then decides whether this text + /// could ever be a handle there, which is the question `resolveHandle` + /// cannot answer: it returns the zero address for a handle nobody + /// holds and for text nobody could hold, and those two must not be + /// confused when money is about to move. + function _requireUsableSlot(bytes32 platformId, string calldata handle) private view returns (bytes32) { + HandleNormalizer.Rules memory rules = _s().names.rulesOf(platformId); + (HandleNormalizer.Problem problem,) = HandleNormalizer.tryNormalize(handle, rules); + if (problem != HandleNormalizer.Problem.None) revert UnusableHandle(problem); + return slotOf(platformId, handle); + } + + // ─── Upgrade ──────────────────────────────────────────────────── + + function _authorizeUpgrade(address) internal override onlyOwner {} + + /// @dev Renouncing would leave no way to repair a broken deployment, and + /// the upgrade is the only lever there is. + function renounceOwnership() public pure override { + revert("renounce disabled"); + } +} diff --git a/solidity/contracts/escrow/IIdentityNames.sol b/solidity/contracts/escrow/IIdentityNames.sol new file mode 100644 index 0000000..c1a13bd --- /dev/null +++ b/solidity/contracts/escrow/IIdentityNames.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {HandleNormalizer} from "../identity/HandleNormalizer.sol"; + +/// @notice The two questions the escrow asks the naming system. +/// +/// @dev A local interface rather than an import of the whole contract, the way +/// `IIdentityVerifier`, `IRegistry` and `INotary` are declared where they +/// are used. Two functions do not justify pulling in the naming contract's +/// whole surface, and a narrow interface says exactly what the escrow +/// depends on. +/// +/// `HandleNormalizer.Rules` IS imported rather than redeclared. A copy +/// would be a second definition of the struct the naming system stores, +/// and the two would drift the first time a field is added. The escrow +/// needs the library anyway, to run the same transform on the text a +/// depositor supplies. +interface IIdentityNames { + /// @notice The wallet that last proved this handle, or the zero address. + /// + /// @dev Zero also means "nobody holds it any more": a handle whose account + /// renamed away is retired, and reads back with no owner. + function resolveHandle(bytes32 platformId, string calldata handle) external view returns (address); + + /// @notice How this platform's handles normalize, as configured now. + /// @dev Reverts for a platform that is not wired. + function rulesOf(bytes32 platformId) external view returns (HandleNormalizer.Rules memory); +} diff --git a/solidity/contracts/escrow/test/HandleEscrow.t.sol b/solidity/contracts/escrow/test/HandleEscrow.t.sol new file mode 100644 index 0000000..9d1dd3c --- /dev/null +++ b/solidity/contracts/escrow/test/HandleEscrow.t.sol @@ -0,0 +1,861 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; +import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import { + ReentrancyGuardTransientUpgradeable +} from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardTransientUpgradeable.sol"; + +import {HandleNormalizer} from "../../identity/HandleNormalizer.sol"; +import {HandleVectors} from "../../identity/HandleVectors.sol"; +import {IdentityNames} from "../../identity/IdentityNames.sol"; +import {IIdentityVerifier} from "../../identity/IIdentityVerifier.sol"; +import {MockIdentityVerifier} from "../../identity/test/MockIdentityVerifier.sol"; +import {MockERC20} from "../../transfer/MockERC20.sol"; +import {HandleEscrow} from "../HandleEscrow.sol"; +import {IIdentityNames} from "../IIdentityNames.sol"; + +/// @notice Takes a cut of every transfer, the way a fee-on-transfer token does. +contract FeeToken is MockERC20 { + uint256 public constant FEE_BPS = 100; // 1% + + constructor() MockERC20("Fee", "FEE") {} + + function transferFrom(address from, address to, uint256 amount) public override returns (bool) { + uint256 fee = (amount * FEE_BPS) / 10_000; + _transfer(from, address(0xdead), fee); + _transfer(from, to, amount - fee); + _spendAllowance(from, msg.sender, amount); + return true; + } +} + +/// @notice A second version that APPENDS to the namespaced root, which is the +/// only change the storage rule allows. +/// +/// @dev Upgrading to a byte-identical implementation proves nothing about the +/// layout. This adds a field after the existing ones and reads the old +/// ones back, so a reordered or removed field shows up as a wrong balance +/// rather than as a passing test. +contract HandleEscrowV2 is HandleEscrow { + /// @custom:storage-location erc7201:libid.storage.HandleEscrow + struct V2Storage { + mapping(bytes32 => mapping(address => uint256)) held; + IIdentityNames names; + uint256 appended; + } + + function _v2() private pure returns (V2Storage storage $) { + assembly { + $.slot := 0xfcca8d7d2c66f78c2760f3fcd99e0bf938b0aeb0d0b471f481dd50b8aff6b400 + } + } + + function setAppended(uint256 v) external { + _v2().appended = v; + } + + function appended() external view returns (uint256) { + return _v2().appended; + } + + /// Read the pre-existing fields through the V2 layout. + function heldThroughV2(bytes32 slot, address token) external view returns (uint256) { + return _v2().held[slot][token]; + } + + function namesThroughV2() external view returns (address) { + return address(_v2().names); + } +} + +/// @notice Refuses every native transfer. +contract RejectEther { + // No receive, no fallback. +} + +/// @notice Calls `deposit` again from inside its own transfer, the way a token +/// with a receiver hook does. +/// +/// @dev This is what the guard on `deposit` is for. The credit is the balance +/// the contract GAINED, measured across the transfer — so a transfer that +/// re-enters and deposits again folds the inner deposit's tokens into the +/// outer one's measurement, and the books end up promising more than the +/// contract holds. +contract ReenteringToken is MockERC20 { + HandleEscrow public escrow; + bytes32 public platformId; + bool private entered; + + constructor() MockERC20("Hook", "HOOK") {} + + function arm(HandleEscrow escrow_, bytes32 platformId_) external { + escrow = escrow_; + platformId = platformId_; + _approve(address(this), address(escrow_), type(uint256).max); + } + + function transferFrom(address from, address to, uint256 amount) public override returns (bool) { + bool ok = super.transferFrom(from, to, amount); + if (!entered && address(escrow) != address(0)) { + entered = true; + escrow.deposit(platformId, "bob", address(this), 1 ether); + } + return ok; + } +} + +/// @notice Calls `claim` again from inside the native payout. +contract ReenteringClaimer { + HandleEscrow private immutable escrow; + bytes32 private immutable platformId; + string private handle; + bool private entered; + + constructor(HandleEscrow escrow_, bytes32 platformId_, string memory handle_) { + escrow = escrow_; + platformId = platformId_; + handle = handle_; + } + + function take() external { + escrow.claim(platformId, handle, address(0), address(this)); + } + + receive() external payable { + if (entered) return; + entered = true; + escrow.claim(platformId, handle, address(0), address(this)); + } +} + +/// @notice The handle-keyed escrow, against the real naming system. +/// +/// @dev Wired to a real `IdentityNames` behind its proxy rather than a mock, so +/// a rename and a recycled handle are stageable exactly as they happen. +contract HandleEscrowTest is Test { + IdentityNames internal names; + MockIdentityVerifier internal verifier; + HandleEscrow internal escrow; + MockERC20 internal token; + + bytes32 internal constant X = HandleVectors.PLATFORM_X; + bytes32 internal constant GITHUB = HandleVectors.PLATFORM_GITHUB; + bytes32 internal constant UNWIRED = keccak256("no such platform"); + + address internal alice = makeAddr("alice"); + address internal bob = makeAddr("bob"); + address internal sender = makeAddr("sender"); + address internal owner = makeAddr("owner"); + + uint32 internal constant V1 = 1; + uint64 internal constant NO_FUTURE_ALLOWANCE = 0; + address internal constant NATIVE = address(0); + + function setUp() public { + IdentityNames namesImpl = new IdentityNames(); + names = IdentityNames( + address(new ERC1967Proxy(address(namesImpl), abi.encodeCall(IdentityNames.initialize, (owner)))) + ); + verifier = new MockIdentityVerifier("mock"); + + vm.startPrank(owner); + names.setPlatform(X, HandleVectors.rulesFor(X)); + names.setVerifier(X, V1, IIdentityVerifier(address(verifier)), NO_FUTURE_ALLOWANCE); + names.setPlatform(GITHUB, HandleVectors.rulesFor(GITHUB)); + names.setVerifier(GITHUB, V1, IIdentityVerifier(address(verifier)), NO_FUTURE_ALLOWANCE); + vm.stopPrank(); + + HandleEscrow escrowImpl = new HandleEscrow(); + escrow = HandleEscrow( + address( + new ERC1967Proxy( + address(escrowImpl), + abi.encodeCall(HandleEscrow.initialize, (owner, IIdentityNames(address(names)))) + ) + ) + ); + + token = new MockERC20("Token", "TKN"); + token.mint(sender, 1_000 ether); + vm.prank(sender); + token.approve(address(escrow), type(uint256).max); + + vm.deal(sender, 100 ether); + vm.warp(1_000_000); + } + + /// Prove `handle` for `who`, the way a login does. + function _bind(address who, string memory userId, string memory handle, uint64 at) internal { + verifier.stage(userId, handle, who, at); + vm.prank(who); + names.bind(X, hex"", false); + } + + function _depositNative(string memory handle, uint256 amount) internal { + vm.prank(sender); + escrow.deposit{value: amount}(X, handle, NATIVE, amount); + } + + // ─── The key ──────────────────────────────────────────────────── + + /// The lemma the whole design rests on: for every handle the naming system + /// accepts, the escrow's key of the raw text and of the normalized text are + /// the SAME slot. If they ever differed, one identity's money would be + /// split across two keys and half of it would be unreachable. + function test_everyAcceptedVectorKeysWithItsNormalizedForm() public view { + HandleVectors.Vector[] memory vectors = HandleVectors.all(); + for (uint256 i = 0; i < vectors.length; i++) { + HandleVectors.Vector memory v = vectors[i]; + if (!v.accepted) continue; + + bytes32 platformId = _platformIdFor(v.platform); + assertEq( + escrow.slotOf(platformId, v.input), + escrow.slotOf(platformId, v.output), + string.concat("vector ", vm.toString(i), " keys differently from its normalized form") + ); + } + } + + /// A literal, so Rust and TypeScript cannot compute a different key and + /// still pass their own tests. + /// + /// Derived from the formula rather than copied out of this contract: + /// keccak256(keccak256("libid.escrow.handle-slot.v1") + /// ‖ keccak256("dyaka.identity.platform.x") + /// ‖ keccak256("alice_1")) + function test_theSlotDerivationIsPinned() public view { + assertEq(escrow.slotOf(X, " Alice_1 "), escrow.slotOf(X, "alice_1")); + assertEq(escrow.slotOf(X, "alice_1"), 0x48d2123d2510af6875a95858dec72232eba05885e8773f13d56b792d6cccc7e2); + } + + function test_theCanonicalFormTrimsAndFolds() public view { + assertEq(escrow.canonicalHandle(" ALICE "), "alice"); + assertEq(escrow.canonicalHandle("Alice_1"), "alice_1"); + // Nothing else is touched: two texts that differ in more than case and + // padding stay two handles. + assertEq(escrow.canonicalHandle("ali-ce"), "ali-ce"); + } + + /// The naming system folds an at-prefixed spelling into the bare handle, + /// so this must too — otherwise one identity's money splits across two + /// keys and half of it is unreachable. + function test_aLeadingAtSignFoldsIntoTheBareHandle() public { + assertEq(escrow.slotOf(X, "@alice"), escrow.slotOf(X, "alice")); + assertEq(escrow.slotOf(X, " @Alice "), escrow.slotOf(X, "alice")); + assertEq(escrow.canonicalHandle("@alice"), "alice"); + // One at-sign, the same as `stripLeadingAt`. A second is a character + // the platform refuses, so the deposit below never lands. + assertEq(escrow.canonicalHandle("@@alice"), "@alice"); + + // Both spellings pay into the same slot, and reading either sees both. + vm.startPrank(sender); + escrow.deposit{value: 1 ether}(X, "@alice", NATIVE, 1 ether); + escrow.deposit{value: 2 ether}(X, "alice", NATIVE, 2 ether); + vm.stopPrank(); + assertEq(escrow.escrowed(X, "@alice", NATIVE), 3 ether); + assertEq(escrow.escrowed(X, "alice", NATIVE), 3 ether); + } + + /// The read pairs with `resolveHandle` on whatever text a user typed: the + /// naming system answers for an at-prefixed spelling, so this must not + /// revert on it. + function test_readingPairsWithTheResolverOnEitherSpelling() public { + // Escrowed first, while nobody holds it — a deposit after the bind + // would pay through and leave nothing to read. + _depositNative("alice", 1 ether); + _bind(alice, "1", "alice", 100); + + assertEq(names.resolveHandle(X, "@alice"), alice); + assertEq(escrow.escrowed(X, "@alice", NATIVE), 1 ether); + } + + /// Text with nothing left after trimming and the at-sign has no slot. + function test_aBareAtSignHasNoSlot() public { + vm.expectRevert(abi.encodeWithSelector(HandleEscrow.UnusableHandle.selector, HandleNormalizer.Problem.Empty)); + escrow.slotOf(X, " @ "); + } + + function test_textWithNothingInItHasNoSlot() public { + vm.expectRevert(abi.encodeWithSelector(HandleEscrow.UnusableHandle.selector, HandleNormalizer.Problem.Empty)); + escrow.slotOf(X, " "); + } + + /// Different platforms are different keyspaces, so the same text on two of + /// them is two slots. + function test_theSlotIsPerPlatform() public view { + assertTrue(escrow.slotOf(X, "alice") != escrow.slotOf(GITHUB, "alice")); + } + + /// The platform id is load-bearing in the key, not decoration: the same + /// text on two platforms is two different people, and their money must not + /// meet. Proving it on one platform reaches only that one's slot. + function test_theSameTextOnTwoPlatformsIsTwoEscrows() public { + vm.startPrank(sender); + escrow.deposit{value: 1 ether}(X, "alice", NATIVE, 1 ether); + escrow.deposit{value: 2 ether}(GITHUB, "alice", NATIVE, 2 ether); + vm.stopPrank(); + + _bind(alice, "1", "alice", 100); // on X only + + vm.prank(alice); + escrow.claim(X, "alice", NATIVE, alice); + assertEq(alice.balance, 1 ether); + + vm.prank(alice); + vm.expectRevert(abi.encodeWithSelector(HandleEscrow.NotTheHolder.selector, address(0), alice)); + escrow.claim(GITHUB, "alice", NATIVE, alice); + + assertEq(escrow.escrowed(GITHUB, "alice", NATIVE), 2 ether, "the other platform's escrow moved"); + } + + /// Why the at-sign is refused rather than handled: the naming system reads + /// both spellings as ONE handle. Keying them apart would split one + /// identity's money across two slots, and only one of them would ever be + /// claimable. + function test_theNamingSystemReadsBothSpellingsAsOneHandle() public { + _bind(alice, "1", "@alice", 100); + + assertEq(names.resolveHandle(X, "alice"), alice); + assertEq(names.resolveHandle(X, "@alice"), alice); + } + + // ─── Depositing ───────────────────────────────────────────────── + + function test_depositHoldsNativeAgainstTheHandle() public { + _depositNative("alice", 1 ether); + + assertEq(escrow.escrowed(X, "alice", NATIVE), 1 ether); + assertEq(address(escrow).balance, 1 ether); + } + + function test_depositHoldsTokensAgainstTheHandle() public { + vm.prank(sender); + escrow.deposit(X, "alice", address(token), 10 ether); + + assertEq(escrow.escrowed(X, "alice", address(token)), 10 ether); + assertEq(token.balanceOf(address(escrow)), 10 ether); + } + + /// Two spellings of one handle land in one slot and add up. + function test_spellingsOfOneHandleAccumulate() public { + _depositNative("alice", 1 ether); + _depositNative(" ALICE ", 2 ether); + + assertEq(escrow.escrowed(X, "alice", NATIVE), 3 ether); + } + + /// The books must never promise more than the contract holds. + function test_aFeeOnTransferTokenCreditsWhatArrived() public { + FeeToken fee = new FeeToken(); + fee.mint(sender, 100 ether); + vm.startPrank(sender); + fee.approve(address(escrow), type(uint256).max); + escrow.deposit(X, "alice", address(fee), 100 ether); + vm.stopPrank(); + + assertEq(escrow.escrowed(X, "alice", address(fee)), 99 ether, "credited more than arrived"); + assertEq(fee.balanceOf(address(escrow)), 99 ether); + } + + /// A handle nobody holds yet is the whole point. + function test_depositingForAnUnclaimedHandleIsFine() public { + assertEq(names.resolveHandle(X, "nobody"), address(0)); + _depositNative("nobody", 1 ether); + assertEq(escrow.escrowed(X, "nobody", NATIVE), 1 ether); + } + + /// The escrow exists for the window before a handle is claimed. Once it + /// resolves, holding the value would only add a claim transaction to reach + /// the same wallet, so the deposit is a payment. + function test_depositForAHeldHandleIsPaidStraightThrough() public { + _bind(alice, "1", "alice", 100); + + uint256 before = alice.balance; + _depositNative("alice", 1 ether); + + assertEq(alice.balance, before + 1 ether, "the holder was not paid"); + assertEq(escrow.escrowed(X, "alice", NATIVE), 0, "the value was escrowed instead"); + assertEq(address(escrow).balance, 0, "the escrow kept it"); + } + + function test_aForwardedDepositIsAnnouncedAsSuch() public { + _bind(alice, "1", "alice", 100); + bytes32 slot = escrow.slotOf(X, "alice"); + + vm.expectEmit(true, true, true, true, address(escrow)); + emit HandleEscrow.Forwarded(slot, NATIVE, sender, alice, X, "alice", 1 ether); + vm.prank(sender); + escrow.deposit{value: 1 ether}(X, "alice", NATIVE, 1 ether); + } + + function test_tokensForAHeldHandleGoStraightToTheHolder() public { + _bind(alice, "1", "alice", 100); + + vm.prank(sender); + escrow.deposit(X, "alice", address(token), 10 ether); + + assertEq(token.balanceOf(alice), 10 ether, "the holder was not paid"); + assertEq(token.balanceOf(address(escrow)), 0, "the escrow kept tokens"); + assertEq(escrow.escrowed(X, "alice", address(token)), 0); + } + + /// The price of paying through: the call depends on the recipient. Failing + /// is the honest outcome — the sender learns, instead of the value waiting + /// in a slot only that same wallet could ever claim. + function test_aHolderThatCannotReceiveFailsTheDeposit() public { + address rejector = address(new RejectEther()); + _bind(rejector, "1", "alice", 100); + + vm.prank(sender); + vm.expectRevert(abi.encodeWithSelector(HandleEscrow.NativeTransferFailed.selector, rejector, 1 ether)); + escrow.deposit{value: 1 ether}(X, "alice", NATIVE, 1 ether); + } + + /// The window the escrow is for: deposit while unclaimed, and the same + /// handle pays through once it is claimed. + function test_theSameHandleEscrowsThenPaysThrough() public { + _depositNative("alice", 1 ether); + assertEq(escrow.escrowed(X, "alice", NATIVE), 1 ether); + + _bind(alice, "1", "alice", 100); + + uint256 before = alice.balance; + _depositNative("alice", 2 ether); + assertEq(alice.balance, before + 2 ether, "the second deposit did not pay through"); + // The first one still waits for its claim. + assertEq(escrow.escrowed(X, "alice", NATIVE), 1 ether, "the waiting balance moved"); + } + + function test_aDepositOfNothingIsRefused() public { + vm.prank(sender); + vm.expectRevert(HandleEscrow.ZeroAmount.selector); + escrow.deposit(X, "alice", NATIVE, 0); + } + + function test_nativeValueMustEqualTheAmount() public { + vm.prank(sender); + vm.expectRevert(abi.encodeWithSelector(HandleEscrow.ValueMismatch.selector, 2 ether, 1 ether)); + escrow.deposit{value: 1 ether}(X, "alice", NATIVE, 2 ether); + } + + /// Ether sent alongside a token deposit has no slot to land in. + function test_aTokenDepositCarriesNoValue() public { + vm.prank(sender); + vm.expectRevert(abi.encodeWithSelector(HandleEscrow.ValueMismatch.selector, 0, 1 ether)); + escrow.deposit{value: 1 ether}(X, "alice", address(token), 10 ether); + } + + /// A mistyped platform id takes nobody's money. + function test_anUnwiredPlatformIsRefused() public { + vm.prank(sender); + vm.expectRevert(abi.encodeWithSelector(IdentityNames.UnknownPlatform.selector, UNWIRED)); + escrow.deposit{value: 1 ether}(UNWIRED, "alice", NATIVE, 1 ether); + } + + /// The reason `rulesOf` exists. Text this platform could never accept would + /// otherwise fund a slot no proof can ever claim, and there is no refund. + function test_aHandleThePlatformCouldNeverAcceptIsRefused() public { + vm.startPrank(sender); + + // A space inside is not a handle on X. + vm.expectRevert(abi.encodeWithSelector(HandleEscrow.UnusableHandle.selector, HandleNormalizer.Problem.BadChar)); + escrow.deposit{value: 1 ether}(X, "ali ce", NATIVE, 1 ether); + + // A hyphen is GitHub's, not X's. + vm.expectRevert(abi.encodeWithSelector(HandleEscrow.UnusableHandle.selector, HandleNormalizer.Problem.BadChar)); + escrow.deposit{value: 1 ether}(X, "ali-ce", NATIVE, 1 ether); + + // Past X's length. + vm.expectRevert(abi.encodeWithSelector(HandleEscrow.UnusableHandle.selector, HandleNormalizer.Problem.TooLong)); + escrow.deposit{value: 1 ether}(X, "a123456789012345", NATIVE, 1 ether); + + vm.stopPrank(); + } + + // ─── Claiming ─────────────────────────────────────────────────── + + function test_theHolderTakesWhatIsHeld() public { + _depositNative("alice", 1 ether); + _bind(alice, "1", "alice", 100); + + vm.prank(alice); + escrow.claim(X, "alice", NATIVE, alice); + + assertEq(alice.balance, 1 ether); + assertEq(escrow.escrowed(X, "alice", NATIVE), 0); + assertEq(address(escrow).balance, 0); + } + + /// The claimer names where it goes, so a wallet that holds the name can pay + /// out somewhere else. + function test_theClaimerChoosesTheRecipient() public { + _depositNative("alice", 1 ether); + _bind(alice, "1", "alice", 100); + + vm.prank(alice); + escrow.claim(X, "alice", NATIVE, bob); + + assertEq(bob.balance, 1 ether); + assertEq(alice.balance, 0); + } + + function test_aClaimTakesOnlyTheTokenItNames() public { + _depositNative("alice", 1 ether); + vm.prank(sender); + escrow.deposit(X, "alice", address(token), 10 ether); + _bind(alice, "1", "alice", 100); + + vm.prank(alice); + escrow.claim(X, "alice", NATIVE, alice); + + assertEq(escrow.escrowed(X, "alice", NATIVE), 0); + assertEq(escrow.escrowed(X, "alice", address(token)), 10 ether, "the token balance moved too"); + } + + /// Any spelling of the handle reaches the same slot. + function test_aClaimCanUseAnySpelling() public { + _depositNative("alice", 1 ether); + _bind(alice, "1", "alice", 100); + + vm.prank(alice); + escrow.claim(X, " ALICE ", NATIVE, alice); + + assertEq(alice.balance, 1 ether); + } + + function test_somebodyElseCannotClaim() public { + _depositNative("alice", 1 ether); + _bind(alice, "1", "alice", 100); + + vm.prank(bob); + vm.expectRevert(abi.encodeWithSelector(HandleEscrow.NotTheHolder.selector, alice, bob)); + escrow.claim(X, "alice", NATIVE, bob); + } + + /// Nobody holds it yet, so nobody can take it. The value waits. + function test_anUnclaimedHandleCannotBeDrained() public { + _depositNative("alice", 1 ether); + + vm.prank(bob); + vm.expectRevert(abi.encodeWithSelector(HandleEscrow.NotTheHolder.selector, address(0), bob)); + escrow.claim(X, "alice", NATIVE, bob); + + assertEq(escrow.escrowed(X, "alice", NATIVE), 1 ether); + } + + function test_claimingAnEmptySlotIsRefused() public { + _bind(alice, "1", "alice", 100); + // Read the slot BEFORE the prank: an external call inside the revert + // argument is itself the next call and would consume it. + bytes32 slot = escrow.slotOf(X, "alice"); + + vm.prank(alice); + vm.expectRevert(abi.encodeWithSelector(HandleEscrow.NothingHeld.selector, slot, NATIVE)); + escrow.claim(X, "alice", NATIVE, alice); + } + + function test_aRecipientThatRefusesNativeValueFailsTheClaim() public { + _depositNative("alice", 1 ether); + _bind(alice, "1", "alice", 100); + address rejector = address(new RejectEther()); + + vm.prank(alice); + vm.expectRevert(abi.encodeWithSelector(HandleEscrow.NativeTransferFailed.selector, rejector, 1 ether)); + escrow.claim(X, "alice", NATIVE, rejector); + } + + /// A token that calls back inside its own transfer would otherwise have the + /// outer deposit credit the inner deposit's tokens as well — two slots + /// funded by one transfer, and more promised than held. + function test_aTokenThatReentersDepositIsRefused() public { + ReenteringToken hook = new ReenteringToken(); + hook.mint(sender, 100 ether); + hook.mint(address(hook), 10 ether); + hook.arm(escrow, X); + + vm.startPrank(sender); + hook.approve(address(escrow), type(uint256).max); + // The guard specifically, not any revert: a mis-staged token or a + // missing approval would also revert and would also look green. + vm.expectRevert( + abi.encodeWithSelector(ReentrancyGuardTransientUpgradeable.ReentrancyGuardReentrantCall.selector) + ); + escrow.deposit(X, "alice", address(hook), 100 ether); + vm.stopPrank(); + + assertEq(escrow.escrowed(X, "alice", address(hook)), 0); + assertEq(escrow.escrowed(X, "bob", address(hook)), 0); + assertEq(hook.balanceOf(address(escrow)), 0, "the escrow kept tokens it never credited"); + } + + /// The payout is an external call to an address the claimer chose. + /// + /// Somebody else's escrow is funded alongside, and that is the point: a + /// second drain is only observable when the contract holds more than the + /// claimed slot. Without it, the second transfer runs out of balance and + /// fails for a reason that has nothing to do with reentrancy. + function test_aReenteringClaimerCannotDrainTwice() public { + ReenteringClaimer claimer = new ReenteringClaimer(escrow, X, "alice"); + _depositNative("alice", 1 ether); + _depositNative("bob", 1 ether); // not the claimer's + _bind(address(claimer), "1", "alice", 100); + + vm.expectRevert(abi.encodeWithSelector(HandleEscrow.NativeTransferFailed.selector, address(claimer), 1 ether)); + claimer.take(); + + assertEq(escrow.escrowed(X, "alice", NATIVE), 1 ether, "the slot was drained"); + assertEq(escrow.escrowed(X, "bob", NATIVE), 1 ether, "somebody else's escrow moved"); + assertEq(address(escrow).balance, 2 ether); + assertEq(address(claimer).balance, 0, "the claimer took anything at all"); + } + + /// A payout to nobody is not a payout. The zero address ACCEPTS a native + /// transfer, so without this the slot would be emptied, the value burned + /// and `Claimed` would report success. + function test_aClaimToNobodyIsRefused() public { + _depositNative("alice", 1 ether); + _bind(alice, "1", "alice", 100); + + vm.prank(alice); + vm.expectRevert(abi.encodeWithSelector(HandleEscrow.BadRecipient.selector, address(0))); + escrow.claim(X, "alice", NATIVE, address(0)); + + assertEq(escrow.escrowed(X, "alice", NATIVE), 1 ether, "the slot was emptied"); + assertEq(address(0).balance, 0, "value was burned"); + } + + /// A payout to this contract would zero the books and leave the value here + /// as surplus no slot points at — unreachable, with no refund and no owner + /// lever. + function test_aClaimBackIntoTheEscrowIsRefused() public { + vm.prank(sender); + escrow.deposit(X, "alice", address(token), 10 ether); + _bind(alice, "1", "alice", 100); + + vm.prank(alice); + vm.expectRevert(abi.encodeWithSelector(HandleEscrow.BadRecipient.selector, address(escrow))); + escrow.claim(X, "alice", address(token), address(escrow)); + + assertEq(escrow.escrowed(X, "alice", address(token)), 10 ether); + assertEq(token.balanceOf(address(escrow)), 10 ether); + } + + /// The ERC-20 payout branch, which no other test reaches: every other + /// claim in this suite takes the native token. + function test_aTokenClaimPaysTheRecipient() public { + vm.prank(sender); + escrow.deposit(X, "alice", address(token), 10 ether); + _bind(alice, "1", "alice", 100); + + vm.prank(alice); + escrow.claim(X, "alice", address(token), bob); + + assertEq(token.balanceOf(bob), 10 ether, "the recipient was not paid"); + assertEq(token.balanceOf(address(escrow)), 0, "the escrow kept tokens"); + assertEq(escrow.escrowed(X, "alice", address(token)), 0); + } + + /// `Deposited` is the only on-chain record tying a slot back to a handle — + /// a slot cannot be turned back into a string — so its payload is worth an + /// assertion of its own. + function test_depositAndClaimAnnounceTheirPayloads() public { + bytes32 slot = escrow.slotOf(X, "alice"); + + vm.expectEmit(true, true, true, true, address(escrow)); + emit HandleEscrow.Deposited(slot, NATIVE, sender, X, "alice", 1 ether); + vm.prank(sender); + escrow.deposit{value: 1 ether}(X, "alice", NATIVE, 1 ether); + + _bind(alice, "1", "alice", 100); + + vm.expectEmit(true, true, true, true, address(escrow)); + emit HandleEscrow.Claimed(slot, NATIVE, alice, bob, 1 ether); + vm.prank(alice); + escrow.claim(X, "alice", NATIVE, bob); + } + + /// A fee-on-transfer token delivers less than was asked for, and the event + /// is the only record of a payment that never entered the books. + function test_aForwardedPaymentReportsWhatArrived() public { + FeeToken fee = new FeeToken(); + fee.mint(sender, 100 ether); + _bind(alice, "1", "alice", 100); + + vm.startPrank(sender); + fee.approve(address(escrow), type(uint256).max); + vm.expectEmit(true, true, true, true, address(escrow)); + emit HandleEscrow.Forwarded(escrow.slotOf(X, "alice"), address(fee), sender, alice, X, "alice", 99 ether); + escrow.deposit(X, "alice", address(fee), 100 ether); + vm.stopPrank(); + + assertEq(fee.balanceOf(alice), 99 ether, "the holder received something else"); + } + + // ─── Consequences accepted on purpose ─────────────────────────── + + /// NOT a vulnerability. The escrow is keyed by the handle and nothing else, + /// so a platform that frees a handle and gives it to somebody new hands the + /// new holder whatever accumulated for the old one. This is the decision, + /// and this test exists so changing it fails here. + function test_ACCEPTED_aRecycledHandlePaysTheNewHolder() public { + // Escrowed while nobody held it, and never claimed. + _depositNative("alice", 1 ether); + _bind(alice, "1", "alice", 100); + + // The platform frees the handle: alice renames away, bob's account + // takes it. + _bind(alice, "1", "alice2", 200); + _bind(bob, "2", "alice", 300); + + vm.prank(bob); + escrow.claim(X, "alice", NATIVE, bob); + + assertEq(bob.balance, 1 ether, "the new holder did not receive it"); + } + + /// NOT a vulnerability. Between a rename and the next proof of the freed + /// handle, the handle resolves to nobody and the slot waits — including + /// against the account that just renamed away from it. + function test_ACCEPTED_aRenamedAwayHandleIsClaimableByNobody() public { + _depositNative("alice", 1 ether); + _bind(alice, "1", "alice", 100); + _bind(alice, "1", "alice2", 200); + + vm.prank(alice); + vm.expectRevert(abi.encodeWithSelector(HandleEscrow.NotTheHolder.selector, address(0), alice)); + escrow.claim(X, "alice", NATIVE, alice); + + // And it did not follow the account to its new name. + assertEq(escrow.escrowed(X, "alice2", NATIVE), 0, "the balance followed the account"); + assertEq(escrow.escrowed(X, "alice", NATIVE), 1 ether); + } + + /// NOT a vulnerability. A deposit is a gift to a name, so the sender has no + /// way back. There is no deadline and no refund by decision. + function test_ACCEPTED_theDepositorCannotTakeItBack() public { + _depositNative("alice", 1 ether); + + vm.prank(sender); + vm.expectRevert(abi.encodeWithSelector(HandleEscrow.NotTheHolder.selector, address(0), sender)); + escrow.claim(X, "alice", NATIVE, sender); + + assertEq(escrow.escrowed(X, "alice", NATIVE), 1 ether); + } + + /// The owner is not a way in either: there is no owner function that moves + /// a balance, and being the owner does not make it the holder. + function test_theOwnerCannotTakeADeposit() public { + _depositNative("alice", 1 ether); + + vm.prank(owner); + vm.expectRevert(abi.encodeWithSelector(HandleEscrow.NotTheHolder.selector, address(0), owner)); + escrow.claim(X, "alice", NATIVE, owner); + } + + // ─── Wiring ───────────────────────────────────────────────────── + + /// Repointing it would redirect every entitlement held, so there is no + /// setter. Moving it is an upgrade, which leaves a record. + function test_theNamingContractIsReadableAndHasNoSetter() public view { + assertEq(address(escrow.names()), address(names)); + } + + function test_initializeRefusesAZeroNamingContract() public { + HandleEscrow impl = new HandleEscrow(); + vm.expectRevert(HandleEscrow.NoNames.selector); + new ERC1967Proxy(address(impl), abi.encodeCall(HandleEscrow.initialize, (owner, IIdentityNames(address(0))))); + } + + function test_ownershipCannotBeRenounced() public { + vm.prank(owner); + vm.expectRevert("renounce disabled"); + escrow.renounceOwnership(); + } + + function test_onlyTheOwnerMayUpgrade() public { + HandleEscrow next = new HandleEscrow(); + + vm.prank(bob); + vm.expectRevert(); + escrow.upgradeToAndCall(address(next), ""); + + vm.prank(owner); + escrow.upgradeToAndCall(address(next), ""); + } + + /// The balances have to survive it, or the upgrade is the theft the pause + /// was refused to avoid. + function test_balancesSurviveAnUpgrade() public { + _depositNative("alice", 1 ether); + bytes32 slot = escrow.slotOf(X, "alice"); + // A version that APPENDS a field, not a copy of the same bytecode: a + // byte-identical upgrade cannot detect a reordered or removed one, + // which is the only mistake this test exists to catch. + HandleEscrowV2 next = new HandleEscrowV2(); + + vm.prank(owner); + escrow.upgradeToAndCall(address(next), ""); + + HandleEscrowV2 upgraded = HandleEscrowV2(payable(address(escrow))); + assertEq(upgraded.heldThroughV2(slot, NATIVE), 1 ether, "the balance moved under the new layout"); + assertEq(upgraded.namesThroughV2(), address(names), "the naming pointer moved"); + assertEq(upgraded.appended(), 0, "the appended field read somebody else's bytes"); + + // The old surface still answers, and the new field is its own slot. + assertEq(escrow.escrowed(X, "alice", NATIVE), 1 ether); + upgraded.setAppended(7); + assertEq(upgraded.appended(), 7); + assertEq(escrow.escrowed(X, "alice", NATIVE), 1 ether, "writing the new field disturbed a balance"); + } + + /// The load-bearing claim of the whole design: a rules change cannot MOVE + /// money already held. Nothing else in this suite calls `setPlatform` + /// twice. + function test_ACCEPTED_aRulesChangeCannotMoveHeldValueButCanStrandIt() public { + _depositNative("alice_9", 1 ether); + bytes32 slot = escrow.slotOf(X, "alice_9"); + _bind(alice, "1", "alice_9", 100); + + // The owner narrows X: no underscore any more. + vm.prank(owner); + names.setPlatform( + X, + HandleNormalizer.Rules({ + maxLength: 15, stripLeadingAt: true, isEmail: false, allowUnderscore: false, allowHyphen: false + }) + ); + + // The key did not move — that is what the rules-independent form buys. + assertEq(escrow.slotOf(X, "alice_9"), slot, "the slot re-keyed"); + assertEq(escrow.escrowedAt(slot, NATIVE), 1 ether, "the value moved"); + + // And what it does NOT buy: the handle no longer resolves, so the + // holder cannot claim until compatible rules return. + assertEq(names.resolveHandle(X, "alice_9"), address(0)); + vm.prank(alice); + vm.expectRevert(abi.encodeWithSelector(HandleEscrow.NotTheHolder.selector, address(0), alice)); + escrow.claim(X, "alice_9", NATIVE, alice); + + // Restoring the rules restores the claim, at the same key. + vm.prank(owner); + names.setPlatform(X, HandleVectors.rulesFor(X)); + vm.prank(alice); + escrow.claim(X, "alice_9", NATIVE, alice); + assertEq(alice.balance, 1 ether); + } + + // ─── Helpers ──────────────────────────────────────────────────── + + function _platformIdFor(string memory platform) internal pure returns (bytes32) { + bytes32 key = keccak256(bytes(platform)); + if (key == keccak256("x")) return HandleVectors.PLATFORM_X; + if (key == keccak256("github")) return HandleVectors.PLATFORM_GITHUB; + if (key == keccak256("google")) return HandleVectors.PLATFORM_GOOGLE; + revert("unknown platform in the vector table"); + } +} diff --git a/solidity/contracts/identity/IdentityNames.sol b/solidity/contracts/identity/IdentityNames.sol index 2cc1731..8651ef6 100644 --- a/solidity/contracts/identity/IdentityNames.sol +++ b/solidity/contracts/identity/IdentityNames.sol @@ -463,6 +463,27 @@ contract IdentityNames is Initializable, UUPSUpgradeable, Ownable2StepUpgradeabl return _s().platforms[platformId].latestVersion; } + /// @notice How this platform's handles normalize, as configured right now. + /// + /// @dev The one part of `Platform` another contract has to be able to read. + /// A contract that keys on handles needs to ask whether some text + /// could be a handle here AT ALL, and `resolveHandle` cannot answer + /// that: it returns the zero address both for a handle nobody holds + /// and for text nobody could ever hold. Without this, such a contract + /// either accepts text that can never resolve, or keeps its own copy + /// of the rules and disagrees with this one the first time `setPlatform` + /// runs. + /// + /// Reverts for a platform that is not wired, like the resolvers do. + /// + /// What is read here is today's configuration, and the owner may + /// change it. A reader that stores anything derived from these rules + /// inherits the re-keying the contract comment describes, so store the + /// rules-independent form and read this only to validate. + function rulesOf(bytes32 platformId) external view returns (HandleNormalizer.Rules memory) { + return _requireUsable(platformId).rules; + } + // ─── Binding ──────────────────────────────────────────────────── /// @notice Prove an identity and bind it to the caller. diff --git a/solidity/contracts/identity/test/IdentityNames.t.sol b/solidity/contracts/identity/test/IdentityNames.t.sol index 97fd3ae..412ba09 100644 --- a/solidity/contracts/identity/test/IdentityNames.t.sol +++ b/solidity/contracts/identity/test/IdentityNames.t.sol @@ -318,6 +318,31 @@ contract IdentityNamesTest is Test { vm.expectRevert(abi.encodeWithSelector(IdentityNames.UnknownPlatform.selector, unwired)); names.resolvePair(unwired, "alice", "123"); + + vm.expectRevert(abi.encodeWithSelector(IdentityNames.UnknownPlatform.selector, unwired)); + names.rulesOf(unwired); + } + + /// `rulesOf` reports the configuration as it stands, so another contract + /// can ask whether text could be a handle here at all — the question + /// `resolveHandle` cannot answer, because it returns the zero address both + /// for a handle nobody holds and for text nobody could hold. + function test_rulesOfReportsThePlatformsCurrentRules() public { + HandleNormalizer.Rules memory rules = names.rulesOf(X); + HandleNormalizer.Rules memory expected = HandleVectors.rulesFor(X); + + assertEq(rules.maxLength, expected.maxLength); + assertEq(rules.stripLeadingAt, expected.stripLeadingAt); + assertEq(rules.isEmail, expected.isEmail); + assertEq(rules.allowUnderscore, expected.allowUnderscore); + assertEq(rules.allowHyphen, expected.allowHyphen); + + // It follows a reconfiguration, rather than reporting what was set + // when the platform was first wired. + vm.prank(owner); + names.setPlatform(X, HandleVectors.rulesFor(GITHUB)); + + assertEq(names.rulesOf(X).allowHyphen, true, "rulesOf did not follow setPlatform"); } function test_resolvePairAgreesWhileOneAccountHoldsBoth() public { diff --git a/solidity/script/Deploy.s.sol b/solidity/script/Deploy.s.sol index d334b72..d6d9e94 100644 --- a/solidity/script/Deploy.s.sol +++ b/solidity/script/Deploy.s.sol @@ -20,6 +20,8 @@ import {XIdentityVerifier} from "../contracts/identity/XIdentityVerifier.sol"; import {GitHubIdentityVerifier} from "../contracts/identity/GitHubIdentityVerifier.sol"; import {GoogleIdentityVerifier} from "../contracts/identity/GoogleIdentityVerifier.sol"; import {IdentityJwksRoots} from "../contracts/identity/IdentityJwksRoots.sol"; +import {HandleEscrow} from "../contracts/escrow/HandleEscrow.sol"; +import {IIdentityNames} from "../contracts/escrow/IIdentityNames.sol"; /// @notice Deploy full stack to any EVM chain. /// @@ -230,6 +232,19 @@ contract Deploy is Script, BankDiamondDeployer { _wireIdentityPlatform(names, HandleVectors.PLATFORM_GOOGLE, googleIdentityAddr); } + // 15. The handle escrow: value sent to a name before anybody claims it. + // + // Wired to the naming proxy and never repointed — moving it would + // redirect every entitlement it holds, so there is no setter and + // changing it is an upgrade. + HandleEscrow escrowImpl = new HandleEscrow(); + address handleEscrowAddr = address( + new ERC1967Proxy( + address(escrowImpl), + abi.encodeCall(HandleEscrow.initialize, (deployer, IIdentityNames(identityNamesAddr))) + ) + ); + vm.stopBroadcast(); console.log("=== Deployment complete ==="); @@ -247,6 +262,7 @@ contract Deploy is Script, BankDiamondDeployer { console.log("GITHUB_IDENTITY_VERIFIER_ADDRESS= ", githubIdentityAddr); console.log("GOOGLE_IDENTITY_VERIFIER_ADDRESS= ", googleIdentityAddr); console.log("IDENTITY_JWKS_ROOTS_ADDRESS= ", jwksRootsAddr); + console.log("HANDLE_ESCROW_ADDRESS= ", handleEscrowAddr); if (googleIdentityAddr != address(0)) { // Nothing is trusted until a notarized reading of Google's JWKS // lands. Until then every Google bind reverts `UntrustedModulus`, diff --git a/ts/packages/contracts/scripts/codegen.mjs b/ts/packages/contracts/scripts/codegen.mjs index e1b4cd3..72ef6b1 100644 --- a/ts/packages/contracts/scripts/codegen.mjs +++ b/ts/packages/contracts/scripts/codegen.mjs @@ -53,6 +53,7 @@ const contracts = [ name: 'GoogleOidcVerifier', exportName: 'googleOidcVerifierAbi', }, + { file: 'HandleEscrow.sol', name: 'HandleEscrow', exportName: 'handleEscrowAbi' }, { file: 'LibidFactory.sol', name: 'LibidFactory', exportName: 'libidFactoryAbi' }, { file: 'MockERC20.sol', name: 'MockERC20', exportName: 'mockErc20Abi' }, { file: 'WTIA9.sol', name: 'WTIA9', exportName: 'wtia9Abi' }, diff --git a/ts/packages/contracts/src/abis/handleEscrow.ts b/ts/packages/contracts/src/abis/handleEscrow.ts new file mode 100644 index 0000000..ffbaa57 --- /dev/null +++ b/ts/packages/contracts/src/abis/handleEscrow.ts @@ -0,0 +1,690 @@ +// Generated by ts/packages/contracts/scripts/codegen.mjs. Do not edit. +// Source artifact: solidity/out/HandleEscrow.sol/HandleEscrow.json + +import type { Abi } from 'viem' + +export const handleEscrowAbi = [ + { + "type": "constructor", + "inputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "NATIVE", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "UPGRADE_INTERFACE_VERSION", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "string", + "internalType": "string" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "acceptOwnership", + "inputs": [], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "canonicalHandle", + "inputs": [ + { + "name": "handle", + "type": "string", + "internalType": "string" + } + ], + "outputs": [ + { + "name": "", + "type": "string", + "internalType": "string" + } + ], + "stateMutability": "pure" + }, + { + "type": "function", + "name": "claim", + "inputs": [ + { + "name": "platformId", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "handle", + "type": "string", + "internalType": "string" + }, + { + "name": "token", + "type": "address", + "internalType": "address" + }, + { + "name": "recipient", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "deposit", + "inputs": [ + { + "name": "platformId", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "handle", + "type": "string", + "internalType": "string" + }, + { + "name": "token", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "payable" + }, + { + "type": "function", + "name": "escrowed", + "inputs": [ + { + "name": "platformId", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "handle", + "type": "string", + "internalType": "string" + }, + { + "name": "token", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "escrowedAt", + "inputs": [ + { + "name": "slot", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "token", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "initialize", + "inputs": [ + { + "name": "owner_", + "type": "address", + "internalType": "address" + }, + { + "name": "names_", + "type": "address", + "internalType": "contract IIdentityNames" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "names", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "contract IIdentityNames" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "owner", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "pendingOwner", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "proxiableUUID", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "renounceOwnership", + "inputs": [], + "outputs": [], + "stateMutability": "pure" + }, + { + "type": "function", + "name": "slotOf", + "inputs": [ + { + "name": "platformId", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "handle", + "type": "string", + "internalType": "string" + } + ], + "outputs": [ + { + "name": "", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "stateMutability": "pure" + }, + { + "type": "function", + "name": "transferOwnership", + "inputs": [ + { + "name": "newOwner", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "upgradeToAndCall", + "inputs": [ + { + "name": "newImplementation", + "type": "address", + "internalType": "address" + }, + { + "name": "data", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [], + "stateMutability": "payable" + }, + { + "type": "event", + "name": "Claimed", + "inputs": [ + { + "name": "slot", + "type": "bytes32", + "indexed": true, + "internalType": "bytes32" + }, + { + "name": "token", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "claimer", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "recipient", + "type": "address", + "indexed": false, + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "Deposited", + "inputs": [ + { + "name": "slot", + "type": "bytes32", + "indexed": true, + "internalType": "bytes32" + }, + { + "name": "token", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "depositor", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "platformId", + "type": "bytes32", + "indexed": false, + "internalType": "bytes32" + }, + { + "name": "handle", + "type": "string", + "indexed": false, + "internalType": "string" + }, + { + "name": "amount", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "Forwarded", + "inputs": [ + { + "name": "slot", + "type": "bytes32", + "indexed": true, + "internalType": "bytes32" + }, + { + "name": "token", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "depositor", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "holder", + "type": "address", + "indexed": false, + "internalType": "address" + }, + { + "name": "platformId", + "type": "bytes32", + "indexed": false, + "internalType": "bytes32" + }, + { + "name": "handle", + "type": "string", + "indexed": false, + "internalType": "string" + }, + { + "name": "amount", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "Initialized", + "inputs": [ + { + "name": "version", + "type": "uint64", + "indexed": false, + "internalType": "uint64" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "OwnershipTransferStarted", + "inputs": [ + { + "name": "previousOwner", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "newOwner", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "OwnershipTransferred", + "inputs": [ + { + "name": "previousOwner", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "newOwner", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "Upgraded", + "inputs": [ + { + "name": "implementation", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "error", + "name": "AddressEmptyCode", + "inputs": [ + { + "name": "target", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "BadRecipient", + "inputs": [ + { + "name": "recipient", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "ERC1967InvalidImplementation", + "inputs": [ + { + "name": "implementation", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "ERC1967NonPayable", + "inputs": [] + }, + { + "type": "error", + "name": "FailedCall", + "inputs": [] + }, + { + "type": "error", + "name": "InvalidInitialization", + "inputs": [] + }, + { + "type": "error", + "name": "NativeTransferFailed", + "inputs": [ + { + "name": "recipient", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "NoNames", + "inputs": [] + }, + { + "type": "error", + "name": "NotInitializing", + "inputs": [] + }, + { + "type": "error", + "name": "NotTheHolder", + "inputs": [ + { + "name": "holder", + "type": "address", + "internalType": "address" + }, + { + "name": "caller", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "NothingHeld", + "inputs": [ + { + "name": "slot", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "token", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "OwnableInvalidOwner", + "inputs": [ + { + "name": "owner", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "OwnableUnauthorizedAccount", + "inputs": [ + { + "name": "account", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "ReentrancyGuardReentrantCall", + "inputs": [] + }, + { + "type": "error", + "name": "SafeERC20FailedOperation", + "inputs": [ + { + "name": "token", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "UUPSUnauthorizedCallContext", + "inputs": [] + }, + { + "type": "error", + "name": "UUPSUnsupportedProxiableUUID", + "inputs": [ + { + "name": "slot", + "type": "bytes32", + "internalType": "bytes32" + } + ] + }, + { + "type": "error", + "name": "UnusableHandle", + "inputs": [ + { + "name": "problem", + "type": "uint8", + "internalType": "enum HandleNormalizer.Problem" + } + ] + }, + { + "type": "error", + "name": "ValueMismatch", + "inputs": [ + { + "name": "expected", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "provided", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "ZeroAmount", + "inputs": [] + } +] as const satisfies Abi diff --git a/ts/packages/contracts/src/abis/identityNames.ts b/ts/packages/contracts/src/abis/identityNames.ts index 08a88e1..83b09f1 100644 --- a/ts/packages/contracts/src/abis/identityNames.ts +++ b/ts/packages/contracts/src/abis/identityNames.ts @@ -390,6 +390,52 @@ export const identityNamesAbi = [ ], "stateMutability": "view" }, + { + "type": "function", + "name": "rulesOf", + "inputs": [ + { + "name": "platformId", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "outputs": [ + { + "name": "", + "type": "tuple", + "internalType": "struct HandleNormalizer.Rules", + "components": [ + { + "name": "maxLength", + "type": "uint16", + "internalType": "uint16" + }, + { + "name": "stripLeadingAt", + "type": "bool", + "internalType": "bool" + }, + { + "name": "isEmail", + "type": "bool", + "internalType": "bool" + }, + { + "name": "allowUnderscore", + "type": "bool", + "internalType": "bool" + }, + { + "name": "allowHyphen", + "type": "bool", + "internalType": "bool" + } + ] + } + ], + "stateMutability": "view" + }, { "type": "function", "name": "setLatestVersion", diff --git a/ts/packages/contracts/src/abis/index.ts b/ts/packages/contracts/src/abis/index.ts index 96ba585..d734754 100644 --- a/ts/packages/contracts/src/abis/index.ts +++ b/ts/packages/contracts/src/abis/index.ts @@ -16,6 +16,7 @@ export { xIdentityVerifierAbi } from './xIdentityVerifier.js' export { identityJwksRootsAbi } from './identityJwksRoots.js' export { xZkVerifierAbi } from './xZkVerifier.js' export { googleOidcVerifierAbi } from './googleOidcVerifier.js' +export { handleEscrowAbi } from './handleEscrow.js' export { libidFactoryAbi } from './libidFactory.js' export { mockErc20Abi } from './mockErc20.js' export { wtia9Abi } from './wtia9.js'