As we learned in Protocol Overview, each pair on Capricorn is actually underpinned by a liquidity pool. Liquidity pools are smart contracts that hold balances of two unique tokens and enforces rules around depositing and withdrawing them. The primary rule is the constant product formula. When a token is withdrawn (bought), a proportional amount must be deposited (sold) to maintain the constant. The ratio of tokens in the pool, in combination with the constant product formula, ultimately determine the price that a swap executes at.
In Capricorn V1, trades are always executed at the "best possible" price, calculated at execution time. Somewhat confusingly, this calculation is actually accomplished with one of two different formulas, depending on whether the trade specifies an exact input or output amount. Functionally, the difference between these two functions is miniscule, but the very existence of a difference increases conceptual complexity. Initial attempts to support both functions in proved inelegant, and the decision was made to not provide any pricing functions in the core. Instead, pairs directly check whether the invariant was satisfied (accounting for fees) after every trade. This means that rather than relying on a pricing function to also enforce the invariant, pairs simply and transparently ensure their own safety, a nice separation of concerns. One downstream benefit is that pairs will more naturally support other flavors of trades which may emerge, (e.g. trading to a specific price at execution time).
At a high level, in Capricorn, trades must be priced in the periphery. The good news is that the library provides a variety of functions designed to make this quite simple, and all swapping functions in the router are designed with this in mind.
When swapping tokens on Capricorn, it's common to want to receive as many output tokens as possible for an exact input amount, or to pay as few input tokens as possible for an exact output amount. In order to calculate these amounts, a contract must look up the current reserves of a pair, in order to understand what the current price is. However, it is not safe to perform this lookup and rely on the results without access to an external price.
Say a smart contract naively wants to send 10 DAI to the DAI/WCUBE pair and receive as much WCUBE as it can get, given the current reserve ratio. If, when called, the naive smart contract simply looks up the current price and executes the trade, it is vulnerable to front-running and will likely suffer an economic loss. To see why, consider a malicious actor who sees this transaction before it is confirmed. They could execute a swap which dramatically changes the DAI/WCUBE price immediately before the naive swap goes through, wait for the naive swap to execute at a bad rate, and then swap to change the price back to what it was before the naive swap. This attack is fairly cheap and low-risk, and can typically be performed for a profit.
To prevent these types of attacks, it's vital to submit swaps that have access to knowledge about the "fair" price their swap should execute at. In other words, swaps need access to an oracle, to be sure that the best execution they can get from Capricorn is close enough to what the oracle considers the "true" price. While this may sound complicated, the oracle can be as simple as an off-chain observation of the current market price of a pair. Because of arbitrage, it's typically the case that the ratio of the intra-block reserves of a pair is close to the "true" market price. So, if a user submits a trade with this knowledge in mind, they can ensure that the losses due to front-running are tightly bounded. This is how, for example, the Capricorn frontend ensure trade safety. It calculates the optimal input/output amounts given observed intra-block prices, and uses the router to perform the swap, which guarantees the swap will execute at a rate no less that x% worse than the observed intra-block rate, where x is a user-specified slippage tolerance (0.5% by default).
There are, of course, other options for oracles, including native V2 oracles.
If you'd like to send an exact amount of input tokens in exchange for as many output tokens as possible, you'll want to use getAmountsOut. The equivalent SDK function is getOutputAmount, or minimumAmountOut for slippage calculations.
If you'd like to receive an exact amount of output tokens for as few input tokens as possible, you'll want to use getAmountsIn. The equivalent SDK function is getInputAmount, or maximumAmountIn for slippage calculations.
For this more advanced use case, see ExampleSwapToPrice.sol.
When integrating Capricorn into another on-chain system, particular care must be taken to avoid security vulnerabilities, avenues for manipulations, and the potential loss of funds.
As a preliminary note: smart contract integrations can happen at two levels: directly with Pair contracts, or through the Router. Direct interactions offer maximal flexibility but require the most work to get right. Mediated interactions offer more limited capabilities but stronger safety guarantees.
There are two primary categories of risk associated with Capricorn. The first involves so-called "static" errors. These can include sending too many tokens to a pair during a swap (or requesting too few tokens back) or allowing transactions to linger in the mempool long enough for the sender's expectations about prices to no longer be accurate.
One may address these errors with fairly straightforward logic checks. Executing these logic checks is the primary purpose of routers. Those who interact directly with pairs must perform these checks themselves (with the help of the Library.
"Dynamic" risk, the second category, involves runtime pricing. Because Cube transactions occur in an adversarial environment, naively written smart contracts can, and will, be exploited for profit. For example, suppose a smart contract checks the asset ratio in a Capricorn pool at runtime and trades against it, assuming that the ratio represents the "fair" or "market" price of these assets. In that case, it is highly vulnerable to manipulation. A malicious actor could, e.g., trivially insert transactions before and after the naive transaction (a so-called "sandwich" attack), causing the smart contract to trade at a radically worse price, profit from this at the trader's expense, and then return the contracts to their original state, all at a low cost. (One important caveat is that these types of attacks are mitigated by trading in highly liquid pools, or at low values.)
The best way to protect against these attacks is to introduce a price oracle. An oracle is any device that returns desired information, in this case, a pair's spot price. The best "oracle" is simply a traders' off-chain observation of the prevailing price, which can be passed into the trade as a safety check. This strategy is best suited to retail trading venues where users initiate transactions on their own behalf. However, it is often the case that a trusted price observation is not available (e.g., in multi-step, programmatic interactions involving Capricorn). Without a price oracle, these interactions will be forced to trade at whatever the (potentially manipulated) rate on Capricorn is. For details on the Capricorn approach to oracles, see Oracles.