Revoking a delegation
POST /v1/revocations removes the delegate from a deposit address: the address becomes a plain EOA again, without
code. Your gas wallet pays the gas, with no fee.
When you need it
Section titled “When you need it”- Users send the native coin to the address from exchanges that use a fixed 21,000 gas limit. Such transfers fail on an address with code: see Deposits from exchanges.
- You stop using Portuna Sweep and want your addresses back as they were.
You do not need it to sweep an address into another hot wallet: sign an authorization for that hot wallet’s delegate and send it with the next sweep.
How it works
Section titled “How it works”The deposit address signs an authorization of the zero address, 0x0000000000000000000000000000000000000000, with
its current nonce. Your gas wallet sends it in a transaction to itself; several revocations go in one transaction.
- The nonce. Each authorization the chain applies raises the address’s nonce by 1, so after the first sweep it is usually 1. If a first sweep of the address is still queued, the revocation’s nonce must be one more.
- Order. A revocation waits until the sweeps of the same address requested before it have gone out.
- Afterwards. Sweeping the address again needs a new authorization of the delegate, like a first sweep.
Sign and send
Section titled “Sign and send”Sign the authorization as for a sweep (see Signing authorizations), with the zero address and the current nonce, then:
curl -X POST "https://api-testnet.portuna.io/v1/revocations" \ -H "Authorization: Bearer $PORTUNA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "external_id": "revoke-2c75", "chain_id": 11155111, "account": "0x2c7536E3605D9C16a7a3D7b1898e529396a65c23", "authorization": { "chain_id": 11155111, "address": "0x0000000000000000000000000000000000000000", "nonce": 1, "y_parity": 1, "r": "0x04bec751666b1f63d7f4572d81a38642d47517b63b84f6b94a0a494a198c315d", "s": "0x2a3cc8c27acdae37a0eb652d394ff698ad3910adbfa4d7a393c49a6d4099cabb" }}'The answer is 202 with the revocation in status queued; follow it with GET /v1/revocations/{id}.
// The nonce the address will have when the revocation goes out: its current one (1 after the first sweep),// plus 1 if its first sweep is still queued.nonce, err := eth.NonceAt(ctx, deposit, nil)if err != nil { log.Fatal(err)}digest, err := portuna.RevocationDigest(sepolia, nonce)if err != nil { log.Fatal(err)}sig, err := crypto.Sign(digest[:], key) // your HSM, KMS or MPC in productionif err != nil { log.Fatal(err)}auth, err := portuna.RevocationFromSignature(sepolia, nonce, deposit, sig)if err != nil { log.Fatal(err)}revocation, _, err := client.CreateRevocation(ctx, portuna.RevocationRequest{ ExternalID: "revoke-" + deposit.Hex(), ChainID: sepolia, Account: deposit, Authorization: auth,})if err != nil { log.Fatal(err)}for revocation.Status == portuna.RevocationQueued || revocation.Status == portuna.RevocationProcessing { time.Sleep(5 * time.Second) if revocation, err = client.Revocation(ctx, revocation.ID); err != nil { log.Fatal(err) }}fmt.Println(revocation.Status, revocation.Reason, revocation.TxHash)// The nonce the address will have when the revocation goes out: its current one (1 after the first sweep),// plus 1 if its first sweep is still queued.const nonce = await rpc.getTransactionCount({ address: deposit })const digest = revocationDigest({ chainId, nonce })const signature = await sign({ hash: digest, privateKey, to: 'hex' }) // your HSM, KMS or MPC in productionconst authorization = await revocationFromSignature({ chainId, nonce, deposit, signature })
let { revocation } = await client.createRevocation({ external_id: `revoke-${deposit}`, chain_id: chainId, account: deposit, authorization,})while (revocation.status === 'queued' || revocation.status === 'processing') { await new Promise((resolve) => setTimeout(resolve, 5000)) revocation = await client.revocation(revocation.id)}console.log(revocation.status, revocation.reason, revocation.tx_hash)In your custody, sign RevocationDigest (revocationDigest) like an authorization digest, and turn the raw signature
into the API’s format with RevocationFromSignature (revocationFromSignature).
Outcomes
Section titled “Outcomes”| Status | reason | What to do |
|---|---|---|
revoked | Done: the address has no code. A revocation.revoked event is sent. | |
rejected | not_delegated | Nothing: the address had no delegation. |
rejected | stale_authorization | The nonce changed. Sign again with the current nonce. |
rejected | invalid_authorization | The authorization did not pass the check before sending. Sign again. |
failed | not_applied | The transaction went through but the chain skipped the authorization: the nonce changed after the check. Sign again. |
failed | another code | The attempts ran out: see causes of a failed attempt, such as insufficient_gas_balance. |
Only gas, from your gas wallet, and no fee. One revocation takes about 28,800 gas on Sepolia under the Glamsterdam
rules and 36,800 under the earlier rules of Ethereum mainnet and BSC; in a transaction with tens of revocations, about
20,000 per address under the earlier rules. gas and gas_cost in the revocation show its share.