Custom Balancer Pool
for SW DAO on
Polygon PoS
A custom liquidity, and market solution utilizing Balancer – offers more accurate pricing for SW DAO products, weighs SWD's price against a basket of assets, and packages the entire liquidity pool within an easily-purchasable token.
Note
Concessions were made to comply with Solidity limitations, and time constraints (ex. contract emits few events, is not gas efficient, and uses an external library). Future versions of this contract should use separate contracts per function, with a local singleton foronSwap()
.
Documentation at this time is minimal: just enough to facilitate audits.
More "user-friendly" documentation is in the works.
Typically, the end-user will never interact with the pool directly.
"User" functions serve to assist in requests for data, and
"owner" functions assist with pool management, but all functionality relevant
to an end-user is abstracted away through Balancer's interfaces. As such, all end-user
functionality is accessible through Balancer, Matcha, and 1inch, but can also be
made available directly through a custom website.
-
Creates a fully-functional Balancer pool, with non-standard features.
-
Allows tokens to be added to, or removed from the pool at will.
-
Manages tokens within configurable categories.
- SW DAO products (SWAP, SWYF, QMB, etc.)
- Common tokens (wETH, LINK, wMATIC, etc.)
- National currencies (USDC, DAI, am3CRV, crvEURTUSD, etc.)
Any token may be placed within these three categories.
-
Allows for changing the weights of these categories, relative to one-another.
-
Allows for changing the weights of individual tokens, relative to other tokens within the same category.
-
Maintains the requested weights using a flat, configurable fee/bonus for trades – offering better prices to traders who bring the pool into balance.
-
Utilizes TPMs for tokens (except for SWD, and the BPT), maintaining fair pricing regardless of balances within the pool.
-
Prices SWD in a constant-product formula against the value of all other assets combined! Making for highly efficient use of liquidity.
-
Allows the BPT to be traded like any other token – with a custom name, and symbol.
-
Implements a "block lock", preventing flash-loan attacks (although no attack vector is currently known).
-
Implements a "swap lock" which can be engaged by the pool's owner to temporarily disable swaps. Also implements a "safe withdrawal" mode for BPT holders while the pool is locked.
-
Transparently pays fees due to the Balancer protocol.
-
Designed to be implemented behind a transparent proxy, making it upgradeable without requiring manual intervention from end-users.
-
Implements contract versioning to assist with upgrades.
-
Implements contract ownership – blocking regular users from sensitive functions, and allowing for safe transfer of ownership.
See "Swap Functionality" below for more detail on how tokens are priced within the pool.
These are only callable by the Balancer vault, and must be accessed using the vault's functionality.
onJoinPool(...)
Standard Balancer interface for joining a pool as a liquidity provider. Used in a nonstandard fashion, and doesn't allow joins from end-users. Instead, joins are performed as standard swaps.onExitPool(...)
Standard Balancer interface for exiting a pool as a liquidity provider, pulling said liquidity. Used in a nonstandard fashion, and only allows end-user exits when the pool is "swap locked". Instead, typical exits are performed as standard swaps.onSwap(...)
Standard Balancer interface for swapping tokens within the pool. Please see "Swap Functionality" below for more detail on how swaps are performed.
See CustomBalancerPool.sol for more detail.
getValue()
Gives the USD value of both the BPT, and SWD (with 18 decimals of precision).isLocked()
Detects whether or not the "swap lock" is engaged.getCirculatingSupply()
Gives the number of BPT in circulation (with 18 decimals of precision).
See CustomBalancerPool.sol for less relevant functions, and more detail.
constructor()
Only exists for compatibility, and goes unused. Never called when run behind a transparent proxy.initialize(string,string)
Fills in for theconstructor()
, and works behind the proxy. Can only be called once per contract "version".tokensAdd(IERC20[],TokenCategory[],uint8[])
Adds tokens to the pool, with specified categories, and weights. Tokens must have TPMs within the designated controller.tokensRemove(IERC20[])
Removes tokens from the pool. Each token must have a zero-balance within the pool.setBalanceFee(uint8)
Sets the flat fee/bonus used to incentivize traders to keep the pool balanced, according to the configured weights.setCategoryWeights(uint8[3])
Sets the weights of each category, relative to one another.setTokenWeights(IERC20[],uint8[])
Sets the weights of the requested tokens. Weights are relative to all other tokens within a given token's category.toggleSwapLock()
Toggles the state of the "swap lock". The lock can always be toggled to "on", but toggling "off" requires that the pool be properly initialized.ownerTransfer(address)
Transfers the pool's ownership from its current owner, to the new address. Must be finalized withownerConfirm()
within 36 hours.ownerConfirm()
Finalizes an ownership transfer.withdrawToken(address)
Rescues mis-sent ERC20 tokens from the contract address.burnPoolTokens(uint)
Burns BPT from the caller's wallet. Only useful for artificially increasing the price of the BPT.
See CustomBalancerPool.sol for more detail.
This repository utilizes Foundry for its developer environment. As a result, building/testing of contracts is relatively simple.
- Install Foundry according to its instructions.
git clone
this repo. with--recurse-submodules
enabled.cd
into the cloned repo. and runforge build
.
- Follow the instructions in the section above.
- Instead of
forge build
, runforge test
.
Tests can be found in the test folder, and are written in Solidity. This repo. has configured
Foundry to fork the Polygon PoS chain during testing. More thorough fuzz testing can be
done by passing the FOUNDRY_PROFILE=fulltest
environment variable before forge test
.
A few methods are available for contract deployment, although doing so is not recommended. The
CustomBalancerPool
is designed to run behind a transparent proxy. Additionally,
deploying, and initializing the contract will register it with the Balancer vault,
connecting it to Balancer's swap interface. Only continue with deployment if you're certain
that's what you need.
Using Foundry
- Follow the instructions in the "Building" section above.
- Run
forge build
, as requested. - Use
forge create
to deploy theExtraStorage
library. - Use
forge create
with--libraries
to deploy theCustomBalancerPool
.
$ forge build
$ forge create <args> contracts/ExtraStorage.sol:ExtraStorage
$ forge create <args> --libraries contracts/ExtraStorage.sol:ExtraStorage:<address> contracts/CustomBalancerPool.sol:CustomBalancerPool
Using Remix
This repo. provides flattened contract code in folders titled "verify". These files correspond
to each of the major contracts, and can be safely ported into Remix for testing, and
deployment. They're also intended for use in Polygonscan verification. Deploy the
ExtraStorage
library first, then point Remix to include the deployed library's
address during deployment of the CustomBalancerPool
.
This section has been placed at the end, due to its length.
The swap implementation is unique, and is composed internally using separate AMM methods:
- The first method applies to all tokens that are not the BPT, or SWD.
- Tokens are bought and sold for the fair price dictated by the TPM.
- Buy versus sell price can be reported differently by the TPM.
- Tokens may run out entirely, and prices do not change according to balance proportions (unlike Uniswap's constant-product method).
- Tokens are maintained according to the set weights using a flat fee/bonus dictated by the configured "balance fee". A user bringing the pool back into balance will receive a bonus (reducing the BPT's price), whereas a user bringing it out of balance will pay a fee (increasing the BPT's price). Balance is incentivized by offering rates different from those in the wider market.
- Fees due to the Balancer protocol are calculated by comparing the final output to a hypothetical, feeless transaction.
- The second method applies to SWD.
- SWD's price is calculated using the constant-product method (like Uniswap).
- Rather than calculating against a single token (ex. WETH, USDC, etc.), SWD is compared to
the entire sum of all other tokens in the pool (in USD).
With the BPT beingToken_0
, and SWD beingToken_1
:Alternatively:Balance: USD Sum: Constant: Token_2 SWD × ... = K Token_N
(SWD Price)×(SWD Balance) = (Token_2 Price)×(Token_2 Balance) + ... + (Token_N Price)×(Token_N Balance)
- Summing the USD value of all tokens can be gas-expensive, therefore future versions of this contract should seek to make this process as efficient as possible, and care should be taken when adding more tokens to the pool. A hard cap of 50 tokens has been set.
- The final method applies to the BPT.
-
Like SWD, the BPT's price is found through the summation of all value within the pool, but rather than changing with balance proportions, the total pool value is simply divided by the BPT's circulating supply.
USD Sum: Balance: USD Price: Token_2 ... / (Circulating Supply) = BPT Token_N
-
The BPT's circulating supply is found by subtracting the pool's balance from the total supply, and then adding those tokens due as fees to the Balancer protocol, but not yet issued.
(Circulating Supply) = (Total Supply) - (Pool Balance) + (Due Protocol Fees)
-
The process of the pool owner joining the pool with
JoinKindPhantom.INIT
mints the initial supply of BPT, and deposits the initial balance within the pool. The total balance remaining in the pool owner's wallet will equate 1-to-1 with every USD (in value) deposited to the pool, excluding the value of the SWD. This gives the BPT an initial value of $1. -
As BPT are accounted for as they enter/leave the pool, the price of the BPT will remain constant for such transactions.
The price of the BPT only changes if:- The underlying tokens change in value.
- Trades are made with the SWD balance (thus changing SWD's value).
- The pool accumulates fees, or grants trade bonuses.
This makes the BPT's performance as an asset easy to understand for the end user.
-
Intuitively, one might expect that the BPT should be valued using double the pool's total value, in order to account for the value of the pool's SWD. However, this would be an error, as shown in the following scenario:
- A user owns all circulating BPT.
- That user sells half their BPT to purchase all tokens except the SWD.
- The user then sells zero BPT to purchase all the SWD which are now worth nothing.
- The user retains half their original BPT, while the pool is now empty.
Accounting for the BPT's price properly avoids such a possibility.
-
All complexity is abstracted away from the end user, and these solutions are compatible with DEx aggregators. Trades from SWD to the BPT are not allowed directly, acting as an artificial bias towards SWD's positive price-action.