For developers / SENDIT

Spot, pUSD and copy contracts

Every one of these is a bank venue or a bank account: money comes from accountOf(msg.sender) and goes back into the bank.

SenditSpotRouter

Buys, sells and launches launched tokens with bank ETH, in front of the existing PepperLauncher, so the tokens sit in the bank where a key can sell them. It is the bank's one capped venue. launch salts the token address with the account, so every account has its own salt space; predictToken(account, ...) tells you the address first. The launcher sees the router as the creator; the router records the account in creatorOf and collect(token) forwards the creator fees (WETH and the token) to that account's wallet. Use the whyNot views before sending.

Launched tokens bought, sold and launched with bank money, in front of the existing PepperLauncher. A capped venue: a key's ETH spent here counts against the account's caps. It holds nothing between calls.

Trading

FunctionWhat it does
buy(address token, uint256 ethIn, uint256 minTokensOut) returns (uint256 tokensOut)Spends ethIn of the account's bank ETH; tokens and any unused ETH land in its bank balance.
sell(address token, uint256 amountIn, uint256 minEthOut) returns (uint256 ethOut)Sells bank-held tokens; the ETH lands in the bank balance.
launch((string name, string symbol, bytes32 imageHash, string description, bytes32 salt, uint256 minTokensOut) a, uint256 ethIn) returns (address token, uint256 tokensOut)Launches with bank ETH (the launch fee, then the creator's first buy). The account is recorded as the creator.
collect(address token) returns (uint256 wethOut, uint256 tokensOut)Permissionless: a router-launched token's creator fees, forwarded to the creator's wallet.
forwardStrayWeth(address token, uint256 amount)The bank's operator: WETH a direct launcher collect left here, to that token's creator and nowhere else.

Reading

FunctionWhat it does
whyNotBuy(address account, address token, uint256 ethIn) view returns (string)Why buy would fail, or "": "paused", the launcher's reason, "balance" or "cap".
whyNotSell(address account, address token, uint256 amountIn) view returns (string)Why sell would fail, or "".
whyNotLaunch(address account, LaunchArgs a, uint256 ethIn) view returns (string)Why launch would fail, or "".
predictToken(address creator, string name, string symbol, bytes32 imageHash, bytes32 salt) view returns (address)Where launch by this account deploys the token.
creatorOf(address token) view returns (address creator)The account that launched a token through the router.
launcher() view returns (address)PepperLauncher.
bank() view returns (address)SenditBank.
weth() view returns (address)WETH.
VERSION() view returns (uint256)1.

Events

EventWhat it does
SpotBought(address indexed account, address indexed token, uint256 ethIn, uint256 tokensOut, uint256 ethRefunded)A buy, naming the account.
SpotSold(address indexed account, address indexed token, uint256 tokensIn, uint256 ethOut)A sell.
LaunchedFor(address indexed token, address indexed creator, uint256 ethIn, uint256 tokensOut)A launch; creator is the account.
CreatorFeesForwarded(address indexed token, address indexed creator, uint256 weth, uint256 tokens)Creator fees sent on to the creator's wallet.

SenditPusdExchange

pUSD for ETH at one dollar, priced by the oracle's ETH/USD round read exactly as the perps engine reads it: pusdOut = ethIn x answer / 10^(18 + decimals - 6), rounded down. No fee. Two doors: buyWithEth (wallet ETH, open since the deploy) and buy (bank ETH, key-signed), which waits for the exchange to become a bank venue: whyNotBuy answers "soon" and venueActiveAt() says when. The reserve is the exchange's own bank balance, so every sale is an in-bank send to the buyer. maxPerTx is 10,000 pUSD at deploy and owner-set.

SenditPusdExchange

pusdExchange

pUSD for ETH at exactly one dollar, priced by the oracle's ETH/USD. One direction, no fee. The pUSD always lands in the buyer's SENDIT balance; the ETH goes to the treasury.

Buying

FunctionWhat it does
buyWithEth(uint256 minPusdOut) payable returns (uint256 pusdOut)The wallet's own ETH (msg.value). Refused for a registered key ("key"): the wallet sends it.
buy(uint256 ethIn, uint256 minPusdOut) returns (uint256 pusdOut)From the SENDIT balance's ETH, key- or wallet-signed. Needs the exchange to be an active bank venue: "soon" until its 24-hour delay has passed.
topUp() returns (uint256 amount)Permissionless: deposits loose pUSD the exchange holds into its reserve.
sweep() returns (uint256 amount)Permissionless: ETH a failed treasury payment left here, to the treasury.

Reading

FunctionWhat it does
quote(uint256 ethIn) view returns (uint256 pusdOut, uint256 price, uint256 updatedAt)What ethIn buys at the latest ETH/USD round, whether or not a buy may use it now.
whyNotBuy(address sender, uint256 ethIn) view returns (string)"paused", "soon", "venue", "stale", "min", "max", "reserve", "balance", or "".
whyNotBuyWithEth(address sender, uint256 ethIn) view returns (string)"paused", "key", "stale", "min", "max", "reserve", or "".
reserve() view returns (uint256)pUSD for sale: the exchange's own bank balance plus loose pUSD.
venueActiveAt() view returns (uint256)When the balance door opens (unix time), 0 once active.
priceDecimals() view returns (uint8)The ETH/USD feed's decimals, 8 on Pickle.
maxPerTx() view returns (uint256)The most pUSD one buy may get. Owner-set; DEFAULT_MAX_PER_TX at deploy.
paused() view returns (bool)The exchange's own pause. The bank's pause stops it too.
treasury() view returns (address)Where the ETH goes.
owner() view returns (address)The owner.
bank() view returns (address)SenditBank.
pusd() view returns (address)pUSD.
oracle() view returns (address)The price feeds.
ethUsdFeedId() view returns (bytes32)The ETH/USD feed id.
VERSION() view returns (uint256)1.
DEFAULT_MAX_PER_TX() view returns (uint256)10,000 pUSD.
IMMEDIATE_MAX_AGE() view returns (uint256)2 seconds: the oldest ETH/USD round a buy accepts.
STALE_GRACE() view returns (uint256)30 seconds added to another feed's heartbeat.
MAX_PRICE_DECIMALS() view returns (uint8)36.
TREASURY_GAS() view returns (uint256)50,000: the gas the treasury payment gets.

Owner

FunctionWhat it does
setMaxPerTx(uint256 maxPerTx_)The per-buy ceiling.
setTreasury(address treasury_)Where the ETH goes.
setPaused(bool paused_)Pauses both doors.
withdrawReserve(address to, uint256 amount)pUSD out of the exchange's own reserve, never anybody else's balance.
transferOwnership(address next)Hands ownership on.

Events

EventWhat it does
Bought(address indexed account, address indexed by, uint256 ethIn, uint256 pusdOut, uint256 price)A buy; by is the signer, price in the feed's decimals.
ReserveToppedUp(address indexed from, uint256 amount)Loose pUSD deposited into the reserve.
ReserveWithdrawn(address indexed to, uint256 amount)The owner took pUSD out of the reserve.
TreasuryPaymentHeld(address indexed treasury, uint256 amount)The treasury refused the ETH; it waits for sweep.
Swept(address indexed treasury, uint256 amount)Held ETH sent to the treasury.
MaxPerTxSet(uint256 maxPerTx)The ceiling changed.
TreasurySet(address indexed treasury)The treasury changed.
PausedSet(bool paused)The pause switched.
OwnershipTransferred(address indexed previous, address indexed next)Ownership changed.

CopyFactory and CopyAccount

CopyFactory.open(leader, amount) creates the copier's CopyAccount for that leader (an EIP-1167 clone at a CREATE2 address, one per pair, for ever) or tops it up, moving amount (at least $10) from the copier's bank balance with bank.move. The copy keeper then calls mirrorOpen for each leader position opened after startedAt, passing the share of capital the leader committed; the account checks market, side and leverage on chain and clamps the share to what the leader's balance allows. Withdrawals pay 5% of the part above principal, half to the leader and half to the FeeRouter. Old copies on an earlier factory are listed in the manifest's copyFactoryPrevious.

CopyFactory

copyFactory

Copy trading: open(leader, amount) makes or re-funds the copier's CopyAccount for that leader (one clone per pair, for ever) and moves the deposit into it from the copier's bank balance.

Copier

FunctionWhat it does
open(address leader, uint256 amount) returns (address account)Starts copying `leader` with amount pUSD (at least MIN_DEPOSIT), or tops up and restarts the pair's account. Key- or wallet-signed.

Reading

FunctionWhat it does
accountOf(address, address) view returns (address)The account of (copier, leader), or 0.
accountsOf(address copier) view returns (address[])Every account a copier opened, stopped ones included.
accountsByLeader(address leader) view returns (address[])Every account copying a leader.
predictAccount(address copier, address leader) view returns (address)The CREATE2 address of a pair's account.
copiersCount(address) view returns (uint256)A leader's live copiers.
isAccount(address) view returns (bool)Whether an address is one of this factory's accounts.
keepers(address) view returns (bool)Whether an address may mirror.
feeRouter() view returns (address)Where the router's half of the copy fee goes.
implementation() view returns (address)The CopyAccount clones copy.
perps() view returns (address)The engine.
orders() view returns (address)The orders book.
pusd() view returns (address)pUSD.
bank() view returns (address)SenditBank.
owner() view returns (address)The owner.
VERSION() view returns (uint256)3.
FEE_BPS() view returns (uint256)500: 5% of withdrawn gains.
LEADER_SHARE_BPS() view returns (uint256)5000: half the fee to the leader.
MIN_DEPOSIT() view returns (uint256)10e6: ten dollars.

Accounts and owner

FunctionWhat it does
onStopped()Called by an account when it stops: the leader's copier count drops.
onRestarted()Called by an account when it restarts.
setKeeper(address keeper, bool allowed)Owner: adds or removes a copy keeper.
setFeeRouter(address next)Owner: the FeeRouter that receives half the fee.
transferOwnership(address next)Owner: hands ownership on.

Events

EventWhat it does
CopyOpened(address indexed copier, address indexed leader, address indexed account, uint256 amount)A copy started or topped up.
KeeperSet(address indexed keeper, bool allowed)A keeper added or removed.
FeeRouterSet(address indexed feeRouter)The FeeRouter changed.
OwnershipTransferred(address indexed previous, address indexed current)Ownership changed.

CopyAccount

copyAccountImpl

One copier copying one leader. Holds the copier's money in its own bank balance and trades it on the engine when, where and how the leader does: same market, side and leverage, the same share of capital, and only leader positions opened after the copy started.

Copier

FunctionWhat it does
stop()No new mirrors; every copied position closes (two-step ones by request). The money stays in the account.
stopAndWithdraw() returns (uint256 amount, uint256 fee)stop, then every free pUSD to the copier's SENDIT balance, and every close that lands later is sent on too (SWEEPING).
withdraw(uint256 amount) returns (uint256 fee)Free pUSD to the copier's SENDIT balance, less the fee on the part above principal.

Keeper, copier or anyone

FunctionWhat it does
mirrorOpen(uint256 leaderPositionId, uint16 fractionBps)Keeper only: copies a leader open with equity x fractionBps / 10000, clamped on chain to what the leader's position can be of the leader's capital. Under one dollar of margin, or no free pUSD, it is skipped.
resolveOpen(uint256 leaderPositionId)Keeper or copier: records a two-step copy that filled or was cancelled.
mirrorClose(uint256 leaderPositionId)Keeper or copier: closes the copy once the leader's position is gone.
sync()Anyone: marks copies the engine already closed (a liquidation, say).
sweep()Anyone, on a SWEEPING account: sends free pUSD to the copier under the fee rule.
fund(uint256 amount)The factory only, after moving the deposit in.
initialize(address copier_, address leader_)The factory only, once.

Reading

FunctionWhat it does
equity() view returns (uint256 total)Free pUSD plus the copied positions' value.
free() view returns (uint256)The account's bank pUSD: what can be withdrawn now.
previewWithdraw(uint256 amount) view returns (uint256 fee, uint256 toLeader, uint256 toRouter)The fee a withdrawal of amount would pay.
principal() view returns (uint256)The copier's money still inside, which is never charged.
deposited() view returns (uint256)Lifetime deposits.
withdrawn() view returns (uint256)Lifetime withdrawals, fee included.
feesPaid() view returns (uint256)Lifetime fees.
mirrors(uint256 leaderPositionId) view returns (uint256 copyPositionId, uint256 requestId, uint8 state)A copy's state, by the leader position it follows.
mirrorOf(uint256 leaderPositionId) view returns (Mirror)The full record: state, leverage, copyPositionId, openRequestId, closeRequestId, margin.
liveMirrors() view returns (uint256[])Leader position ids with a live copy.
liveCount() view returns (uint256)How many, at most MAX_LIVE.
copier() view returns (address)The copier's account.
leader() view returns (address)The leader's account.
startedAt() view returns (uint64)Leader positions opened before this are not copied.
stopped() view returns (bool)Stopped.
sweeping() view returns (bool)Asked for everything back.
factory() view returns (address)The factory.
perps() view returns (address)The engine.
orders() view returns (address)The orders book.
pusd() view returns (address)pUSD.
bank() view returns (address)SenditBank.
VERSION() view returns (uint256)3.
MAX_LIVE() view returns (uint256)64 copied positions at once.
STATE_NONE() view returns (uint8)0: never seen.
STATE_PENDING() view returns (uint8)1: a two-step open is waiting.
STATE_OPEN() view returns (uint8)2: the copy is live.
STATE_CLOSING() view returns (uint8)3: a two-step close was sent.
STATE_CLOSED() view returns (uint8)4: over.
STATE_SKIPPED() view returns (uint8)5: never opened (too small, no free pUSD, refused, or cancelled).

Events

EventWhat it does
Deposited(uint256 amount)Money in.
Withdrawn(uint256 amount, uint256 fee, uint256 toLeader, uint256 toRouter)The copier took money out.
Swept(uint256 amount, uint256 fee, uint256 toLeader, uint256 toRouter)A SWEEPING account paid its copier when a close landed.
MirrorOpened(uint256 indexed leaderPositionId, uint256 copyPositionId, uint256 requestId, uint256 margin, uint16 leverage)A copy opened, or requested on a two-step market.
MirrorResolved(uint256 indexed leaderPositionId, uint256 copyPositionId)A two-step copy settled.
MirrorClosed(uint256 indexed leaderPositionId, uint256 copyPositionId, uint256 requestId)A copy closed, or its close requested.
MirrorSkipped(uint256 indexed leaderPositionId, string reason)A leader open that was not copied, and why.
Stopped()Copying stopped.
Restarted(uint256 startedAt)A top-up restarted a stopped account.
SweepArmed()stopAndWithdraw marked the account SWEEPING.

pUSD

The test dollar: six decimals, minted by the faucet operator, not backed by anything. Trusted spenders (the bank among them) move it without an approve, so no trade needs a second signature.

ERC-20

FunctionWhat it does
transfer(address to, uint256 value) returns (bool)Transfer.
transferFrom(address from, address to, uint256 value) returns (bool)Transfer by allowance, or by a trusted spender with none.
approve(address spender, uint256 value) returns (bool)Approve.
balanceOf(address) view returns (uint256)Wallet balance. pUSD in SENDIT is in the bank, not here.
allowance(address holder, address spender) view returns (uint256)type(uint256).max for a trusted spender.
totalSupply() view returns (uint256)Supply.
name() view returns (string)Name.
symbol() view returns (string)"pUSD".
decimals() view returns (uint8)6.
trustedSpender(address) view returns (bool)Whether an address moves pUSD with no approve.

Minter and owner

FunctionWhat it does
mint(address to, uint256 value)The minter (the faucet operator), at most MAX_MINT per call.
minter() view returns (address)The minter.
owner() view returns (address)The owner.
setMinter(address next)Owner: a new minter.
setTrustedSpender(address spender, bool trusted)Owner: adds or removes a trusted spender.
transferOwnership(address next)Owner: hands ownership on.
MAX_MINT() view returns (uint256)100,000 dollars per mint.
VERSION() view returns (uint256)The contract version.

Events

EventWhat it does
Transfer(address indexed from, address indexed to, uint256 value)Transfer.
Approval(address indexed owner, address indexed spender, uint256 value)Approval.
MinterChanged(address indexed previous, address indexed current)New minter.
TrustedSpenderSet(address indexed spender, bool trusted)A trusted spender added or removed.
OwnershipTransferred(address indexed previous, address indexed current)Ownership changed.