開発者へ / SENDIT
SenditBank
一つのコントラクトが SENDIT のすべてのアカウントのお金を保持します。ウォレットは一度入金してキーを登録し、その後はキーが取引に署名し、すべてのベニューがそのキーをウォレットのアカウントへと解決します。
モデル
- アカウント。アカウントはウォレットのアドレス(または自分自身のアカウントとして振る舞う任意のコントラクト)です。残高は
balanceOf[account][asset]であり、キーで索引付けされることはありません。 - 資産。pUSD、
address(0)としての ETH、そして運営者が追加したランチャーのトークン(isAsset)。それ以外のトークンは拒否されます。totalHeld[asset]はすべての残高の正確な合計であり、バンクが保有する額を超えることはありません。 - キー。アカウントが、そのキー自身が署名した証明とともに登録したアドレスです。キーはベニューでアカウントの残高を使い、送金し、アカウント自身のウォレットへ引き出し、自分のガスを補充し、無効化できます。キーの登録、上限の変更、別のアドレスへの
withdrawToはできません。 - ベニュー。アカウントから引き落とすことを許されたコントラクトです。エンジン、注文ブック、ボールト、コピーファクトリー、スポットルーター、pUSD エクスチェンジ。各ベニューは自分の呼び出し元を渡して
bank.pull(msg.sender, asset, amount)を求めるので、ベニューが誰のお金を取るかを指定することはできません。すべての払い出しはbank.push(account, ...)で戻り、署名者に支払われることはありません。
receive() も、資金の一括回収も、残高の編集も、アップグレードも、手数料もありません。ETH が入ってくるのは deposit と push を通じてだけです。
アカウントと accountOf
accountOf(sender) は、送信者が代理で操作するアカウントです。sender がアカウントの現在の keyEpoch と現在の globalEpoch の下で登録されたキーであればそのオーナーを、そうでなければ sender 自身を返します。そのためウォレットは自分自身に解決され、すべてのベニューはウォレットの署名でも動きます。ポジション、ボールトの持ち分、コピーアカウント、そしてすべてのイベントが示すのはアカウントであり、キーではありません。
accountOf(keyOf(account)) != account のとき、キーは古くなっています。ここに playerOf はありません。それはアーケードのバンクで同じ考え方に付けられた名前です。
キーの証明
キーは、ダイジェストに署名することでアカウントに仕えることに同意します。
keyDigest(account, key) = keccak256(abi.encode( "SENDIT/key-proof/v1", block.chainid, address(bank), account, key, keyEpoch[account], globalEpoch))
証明は、その 32 バイトに対するキーの EIP-191 署名("\x19Ethereum Signed Message:\n32" || digest)です。65 バイトで、s は小さい値、v は 27 または 28 です(0 と 1 も受け付けます)。ウォレットはそれを setKey(key, proof) で、または deposit(pusdAmount, gasToKey, key, proof) の中で送ります。後者は同じトランザクションでキーのガス代を入金することもできます。
import { encodeAbiParameters, parseAbiParameters, keccak256 } from "viem";
import { generatePrivateKey, privateKeyToAccount } from "viem/accounts";
const key = privateKeyToAccount(generatePrivateKey()); // keep the private key safe
// ADD a key under the epochs in force: the bank computes the digest for you.
const digest = await client.readContract({
address: bank, abi: bankAbi, functionName: "keyDigest", args: [account, key.address],
});
const proof = await key.signMessage({ message: { raw: digest } }); // EIP-191, 32 bytes
// REPLACE every key of the account: the same digest under keyEpoch + 1.
const [epoch, global] = await Promise.all([
client.readContract({ address: bank, abi: bankAbi, functionName: "keyEpoch", args: [account] }),
client.readContract({ address: bank, abi: bankAbi, functionName: "globalEpoch" }),
]);
const nextDigest = keccak256(encodeAbiParameters(
parseAbiParameters("string, uint256, address, address, address, uint64, uint64"),
["SENDIT/key-proof/v1", 78270n, bank, account, key.address, epoch + 1n, global],
));
const replaceProof = await key.signMessage({ message: { raw: nextDigest } });SENDIT 自身のフロントエンドは、固定の EIP-4361 メッセージ("Unlock my SENDIT trading key..."、ドメイン、チェーン、バンク、両方のエポックに結び付けられたもの)へのウォレットの署名からキーを導出するので、どのデバイスでも同じキーが導出し直されます。ボットにそれは必要ありません。証明が正しければ、あなたが持っている任意のキーが使えます。
エポック:追加か置き換えか
証明は二つのエポックの組のどちらかに照らして検証され、どちらであるかによって何が起こるかが決まります。
- 現在のエポック:追加。キーはアカウントの有効なキーに加わり、ほかのキーもすべて有効なままです(デバイスごとに一つのキー)。
- keyEpoch + 1:置き換え。バンクはまず無効化し(エポックが進み、すべてのキーが終わり、
KeyRevoked)、それから新しいエポックの下でキーを登録します。
revokeKey() はアカウントのすべてのキーを終わらせます。rotateKey(newKey, proof) は、新しいエポックの下での証明を使って、無効化と登録を一つのトランザクションで行います。invalidateAllKeys() は globalEpoch を進め、バンク上のすべてのキーを終わらせます。keyOf(account) は最後に登録されたキーにすぎず、入金のガスはそこに送られます。
上限
キーによる第三者への pUSD と ETH の流出(send、refuel、そして上限付きのベニューによる pull。現在それはスポットルーターです)は、アカウントごとの 1 日あたりの上限に対して数えられます。期間は、最初に計上された流出から始まる固定の 24 時間です。パーペチュアル、ボールト、コピーには上限がなく、ローンチされたトークンも同様で、ウォレットの署名による呼び出しが計上されることはありません。コントラクトの既定値は 1 日あたり 10,000 pUSD と 5 ETH ですが、テストネットのデプロイではオフにしています(UNCAPPED。defaultCaps() を読んでください)。アカウントは自分のウォレットから setKeyCaps で独自の上限を設定でき、0 は既定値を意味します。
一時停止
setPaused(true) は、ベニューの pull と move、そしてキーが署名する send と refuel を止めます。withdraw、withdrawTo、push(そのためポジションは引き続き決済され、払い出しも届きます)、入金、キーの登録と無効化、ウォレットやコントラクト自身の send(コピーアカウントは send を通じてコピーした人に支払います)は決して止めません。pUSD エクスチェンジは、その二つの入口の両方でバンクの一時停止に従います。
ベニューと 24 時間の遅延
ベニューは、自分が渡す送信者を通じて任意のアカウントから引き落とせるので、ベニューはバンクの信頼の根です。追加は proposeVenue を行い、VENUE_DELAY(24 時間)の後に activateVenue を行います。VenueProposed イベントにより、すべてのアカウントには先に引き出すための 1 日が与えられます。listVenueNow が待機を省けるのは、バンクの最初の入金より前だけです。removeVenue は即時で、削除されたベニューも push は引き続き行えます。
関数とイベント
SenditBank
bankWhere every SENDIT account's money lives: pUSD, ETH (address(0)) and launched tokens, keyed by the account (the wallet), never by a key. Venues debit it with pull and credit it with push.
Wallet
| Function | What it does |
|---|---|
| deposit(uint256 pusdAmount, uint256 gasToKey, address key, bytes keyProof) payable | The one wallet transaction: pulls pUSD with no approve, credits msg.value - gasToKey as ETH, registers key when non-zero, and sends gasToKey to the account's key. Every argument may be zero. |
| depositToken(address token, uint256 amount) | Deposits a launched token (or pUSD) already in the wallet. A launched token needs an approve to the bank first; pUSD does not. |
| setKey(address key, bytes keyProof) | Registers a key. A proof under the current epochs ADDS it; a proof under keyEpoch + 1 REPLACES every key of the account. |
| setKeyCaps(uint128 pusdPerDay, uint128 ethPerDay) | The account's own daily caps on its keys' sends, spot buys and launches. 0 means the default, UNCAPPED means none. Wallet only. |
| withdrawTo(address asset, uint256 amount, address to) | Wallet only: pays another address, for a smart account with no receive(). Never paused, never capped. |
Wallet or key
| Function | What it does |
|---|---|
| withdraw(address asset, uint256 amount) | Pays accountOf(msg.sender), the wallet, whoever signs. Never paused, never capped. |
| send(address to, address asset, uint256 amount, bool toWallet) | From the signer's account to `to`: credited in the bank (toWallet false; a live key named as `to` credits its account) or paid out as the asset. Key-signed pUSD and ETH count against the caps and stop while paused. |
| revokeKey() | Ends every key of the account: keyEpoch moves on, so every device derives a new key. |
| rotateKey(address newKey, bytes keyProof) | revokeKey, then registers newKey with a proof under the NEW account epoch, in one transaction. |
| refuel() returns (uint256 amount) | A live key below REFUEL_BELOW tops itself up to REFUEL_TO from its account's ETH, once per REFUEL_EVERY. Stops while paused. |
Venues
| Function | What it does |
|---|---|
| pull(address sender, address asset, uint256 amount) returns (address account) | A venue debits accountOf(sender) and receives the asset. The venue passes its own msg.sender, so it never names whose money it takes. Stops while paused. |
| push(address account, address asset, uint256 amount) payable | A venue credits `account` with what it hands over in this call. Never paused, and a removed venue can still push, so payouts in flight land. |
| move(address sender, address to, address asset, uint256 amount) returns (address account) | A venue debits accountOf(sender) and credits `to` with no token moving (how a copy account is funded). Stops while paused. |
Reading an account
| Function | What it does |
|---|---|
| accountOf(address sender) view returns (address) | The account behind a sender: the owner while sender is one of its live keys, sender itself otherwise. |
| balanceOf(address account, address asset) view returns (uint256) | One balance. asset address(0) is ETH. |
| keyOf(address account) view returns (address) | The key registered last (address(0) after a revoke). Not proof that it is live: compare accountOf(keyOf(account)) with account. |
| ownerOfKey(address key) view returns (address) | The account a key was registered for, live or not. |
| keyEpoch(address account) view returns (uint64) | The account's key epoch. A revoke adds one. |
| keyAccountEpochOf(address key) view returns (uint64) | The account epoch a key was registered under. |
| keyGlobalEpochOf(address key) view returns (uint64) | The global epoch a key was registered under. |
| globalEpoch() view returns (uint64) | The bank-wide epoch. invalidateAllKeys adds one. |
| keyDigest(address account, address key) view returns (bytes32) | What the key signs (EIP-191) to consent to serving the account under the epochs in force now. |
| keyCapsOf(address account) view returns (uint256 pusdPerDay, uint256 ethPerDay, uint256 pusdSpent, uint256 ethSpent, uint64 windowEnds) | The caps in force and what the keys spent in the current 24-hour window (zeros once it ended). |
| lastRefuelAt(address key) view returns (uint64) | When a key last refuelled. |
| totalHeld(address asset) view returns (uint256) | The sum of every balance of an asset. Never more than the bank holds. |
| isAsset(address asset) view returns (bool) | ETH, pUSD, or a token launched by a launcher the operator added. |
Reading the bank
| Function | What it does |
|---|---|
| pusd() view returns (address) | The pUSD token. |
| operator() view returns (address) | The operator: pause, venues, default caps, launchers, the key brake. |
| paused() view returns (bool) | Whether venue pulls and moves and key-signed sends and refuels are stopped. |
| everFunded() view returns (bool) | Set by the first credit; closes listVenueNow for ever. |
| defaultCaps() view returns (uint128 pusdPerDay, uint128 ethPerDay) | The caps an account gets when it set none. |
| isVenue(address) view returns (bool) | Whether an address may pull and move. |
| isCappedVenue(address) view returns (bool) | Whether a key's pUSD and ETH spent through this venue count against the caps (the spot router). |
| wasVenue(address) view returns (bool) | Ever listed: may still push after a removal. |
| venueActiveAt(address) view returns (uint64) | When a proposed venue may be activated; 0 when none is pending. |
| proposedCapped(address) view returns (bool) | The capped flag a pending venue will be listed with. |
| isLauncher(address) view returns (bool) | Whether a launcher's tokens are accepted. |
| launchers(uint256) view returns (address) | The added launchers, by index. |
| launcherCount() view returns (uint256) | How many launchers were added. |
Operator
| Function | What it does |
|---|---|
| proposeVenue(address venue, bool capped) | Starts the VENUE_DELAY wait before a venue can be listed. |
| activateVenue(address venue) | Lists a proposed venue once its delay has passed. |
| listVenueNow(address venue, bool capped) | Lists a venue at once, only while the bank has never been credited (how the deploy lists its own venues). |
| removeVenue(address venue) | Immediate. The venue can no longer pull or move, but can still push. |
| setPaused(bool paused_) | The pause (see the pause rules). |
| setDefaultCaps(uint128 pusdPerDay, uint128 ethPerDay) | The default caps. Both must be non-zero. |
| addLauncher(address launcher) | Add-only: a launcher's tokens stay accepted for ever. |
| invalidateAllKeys() | Moves globalEpoch on: every key on the bank stops resolving at once. Moves no balance; costs every account one setKey. |
| setOperator(address next) | Hands the operator role on. |
Constants
| Function | What it does |
|---|---|
| VERSION() view returns (uint256) | 1. |
| ETH() view returns (address) | address(0), the ETH asset. |
| VENUE_DELAY() view returns (uint64) | 24 hours between proposeVenue and activateVenue. |
| CAP_WINDOW() view returns (uint64) | 24 hours, from the first charged outflow. |
| UNCAPPED() view returns (uint128) | type(uint128).max: a cap of this value is no cap. |
| REFUEL_BELOW() view returns (uint256) | 0.002 ETH: a key may refuel below this. |
| REFUEL_TO() view returns (uint256) | 0.01 ETH: what a refuel tops the key up to. |
| REFUEL_EVERY() view returns (uint256) | 1 hour between two refuels of one key. |
Events
| Event | What it does |
|---|---|
| Deposited(address indexed account, address indexed asset, uint256 amount) | A deposit credited, pUSD, ETH or a token. |
| GasSent(address indexed account, address indexed key, uint256 amount, bool fromBalance) | ETH sent to a key: the deposit's gas seed (fromBalance false) or a refuel (true). |
| Withdrawn(address indexed account, address indexed asset, uint256 amount, address to, address by) | A withdrawal; `by` is the signer, a key or the wallet. |
| Sent(address indexed from, address indexed to, address indexed asset, uint256 amount, bool toWallet, address by) | A send, in the bank or paid out. |
| Pulled(address indexed venue, address indexed account, address indexed asset, uint256 amount, address by) | A venue took money from an account; `by` is who signed. |
| Pushed(address indexed venue, address indexed account, address indexed asset, uint256 amount) | A venue paid an account. |
| Moved(address indexed venue, address indexed from, address indexed to, address asset, uint256 amount, address by) | A venue moved money between two accounts. |
| KeySet(address indexed account, address indexed key, uint64 accountEpoch, uint64 globalEpoch) | A key registered. |
| KeyRevoked(address indexed account, address indexed key, uint64 nextEpoch, address by) | Every key of an account ended. |
| KeysInvalidated(uint64 globalEpoch) | The operator ended every key on the bank. |
| CapsSet(address indexed account, uint256 pusdPerDay, uint256 ethPerDay) | Caps changed; account address(0) is the default. |
| VenueProposed(address indexed venue, bool capped, uint64 activeAt) | A venue proposed; activeAt is when it can be listed. |
| VenueSet(address indexed venue, bool allowed, bool capped) | A venue listed or removed. |
| Paused(bool paused) | The pause switched. |
| OperatorSet(address indexed previous, address indexed next) | The operator changed. |
| LauncherAdded(address indexed launcher) | A launcher's tokens accepted. |