개발자를 위한 문서 / 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가 없습니다. 그것은 아케이드 뱅크에서 같은 개념을 부르는 이름입니다.

키 증명

키는 다이제스트에 서명함으로써 계정을 위해 일하는 데 동의합니다.

Solidity
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바이트, low s, v는 27 또는 28입니다(0과 1도 받아들입니다). 그다음 지갑이 setKey(key, proof)로, 또는 deposit(pusdAmount, gasToKey, key, proof) 안에 넣어 보내며, 후자는 같은 트랜잭션에서 키의 가스도 채울 수 있습니다.

viem
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...", 도메인, 체인, 뱅크, 두 에포크에 묶여 있음)에 대한 지갑 서명으로부터 키를 도출하므로, 모든 기기가 같은 키를 다시 도출합니다. 봇에는 그것이 필요 없습니다. 증명만 올바르다면 여러분이 가진 어떤 키든 작동합니다.

에포크: 추가 또는 교체

증명은 두 에포크 쌍 중 하나에 대해 검사되며, 어느 쪽이냐에 따라 일어나는 일이 정해집니다.

  • 현재 에포크: 추가(ADD). 키가 계정의 살아 있는 키에 합류하며, 다른 모든 키도 살아 있습니다(기기마다 키 하나).
  • keyEpoch + 1: 교체(REPLACE). 뱅크가 먼저 폐기하고(에포크가 넘어가고 모든 키가 끝나며 KeyRevoked), 그다음 새 에포크 아래에 키를 등록합니다.

revokeKey()는 계정의 모든 키를 끝냅니다. rotateKey(newKey, proof)는 새 에포크 아래의 증명으로 폐기와 등록을 한 트랜잭션에서 합니다. invalidateAllKeys()는 globalEpoch를 넘기고 뱅크의 모든 키를 끝냅니다. keyOf(account)는 마지막으로 등록된 키, 즉 입금의 가스가 가는 키일 뿐입니다.

한도

키가 제3자에게 내보내는 pUSD와 ETH(send, refuel, 그리고 한도가 걸린 베뉴의 pull, 현재는 현물 라우터)는 계정별 하루 한도에 산입되며, 그 기간은 처음 산입된 유출부터 고정된 24시간 창입니다. 무기한 선물, 볼트, 카피에는 한도가 없고 출시된 토큰도 마찬가지이며, 지갑 서명 호출은 결코 산입되지 않습니다. 컨트랙트 기본값은 하루 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 이벤트가 모든 계정에 먼저 출금할 하루를 줍니다. listVenueNow는 뱅크에 처음으로 잔액이 기록되기 전에만 대기를 건너뜁니다. removeVenue는 즉시 이루어지며, 제거된 베뉴도 여전히 push할 수 있습니다.

함수와 이벤트

Where 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

FunctionWhat it does
deposit(uint256 pusdAmount, uint256 gasToKey, address key, bytes keyProof) payableThe 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

FunctionWhat 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

FunctionWhat 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) payableA 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

FunctionWhat 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

FunctionWhat 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

FunctionWhat 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

FunctionWhat 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

EventWhat 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.