├── .gitignore ├── LICENSE ├── README.md ├── balances.json ├── contracts ├── flexr-stx-token.clar ├── flexr-token.clar ├── geyser.clar ├── oracle.clar ├── plaid-stx-token.clar ├── plaid-token.clar ├── src20-trait.clar ├── stx-token.clar ├── swapr-trait.clar ├── swapr.clar └── wrapr-token.clar ├── deploy-contracts.ts ├── package.json ├── scenario.md ├── src ├── clients │ ├── flexr-client.ts │ ├── flexr-stx-token-client.ts │ ├── geyser-client.ts │ ├── oracle-client.ts │ ├── plaid-client.ts │ ├── plaid-stx-token-client.ts │ ├── stx-client.ts │ ├── swapr-client.ts │ └── wrapr-client.ts ├── errors.ts └── utils.ts ├── swapr.clarinet ├── Clarinet.toml ├── contracts │ ├── plaid-stx-token.clar │ ├── plaid-token.clar │ ├── sip-010.clar │ ├── stx-token.clar │ ├── swapr-trait.clar │ └── swapr.clar ├── history.txt ├── settings │ └── Development.toml └── tests │ ├── plaid-stx-token_test.js │ ├── plaid-token_test.js │ ├── stx-token_test.js │ ├── swapr_test.js │ └── utils.js ├── test ├── mocha.opts └── unit │ ├── flexr.ts │ └── providerWithInitialAllocations.ts ├── tsconfig.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | keychain.json 2 | 3 | # Logs 4 | lib 5 | logs 6 | *.log 7 | npm-debug.log* 8 | yarn-debug.log* 9 | yarn-error.log* 10 | lerna-debug.log* 11 | 12 | # Diagnostic reports (https://nodejs.org/api/report.html) 13 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 14 | 15 | # Runtime data 16 | pids 17 | *.pid 18 | *.seed 19 | *.pid.lock 20 | 21 | # Directory for instrumented libs generated by jscoverage/JSCover 22 | lib-cov 23 | 24 | # Coverage directory used by tools like istanbul 25 | coverage 26 | *.lcov 27 | 28 | # nyc test coverage 29 | .nyc_output 30 | 31 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 32 | .grunt 33 | 34 | # Bower dependency directory (https://bower.io/) 35 | bower_components 36 | 37 | # node-waf configuration 38 | .lock-wscript 39 | 40 | # Compiled binary addons (https://nodejs.org/api/addons.html) 41 | build/Release 42 | 43 | # Dependency directories 44 | node_modules/ 45 | jspm_packages/ 46 | 47 | # TypeScript v1 declaration files 48 | typings/ 49 | 50 | # TypeScript cache 51 | *.tsbuildinfo 52 | 53 | # Optional npm cache directory 54 | .npm 55 | 56 | # Optional eslint cache 57 | .eslintcache 58 | 59 | # Optional REPL history 60 | .node_repl_history 61 | 62 | # Output of 'npm pack' 63 | *.tgz 64 | 65 | # Yarn Integrity file 66 | .yarn-integrity 67 | 68 | # dotenv environment variables file 69 | .env 70 | .env.test 71 | 72 | # parcel-bundler cache (https://parceljs.org/) 73 | .cache 74 | 75 | # next.js build output 76 | .next 77 | 78 | # nuxt.js build output 79 | .nuxt 80 | 81 | # vuepress build output 82 | .vuepress/dist 83 | 84 | # Serverless directories 85 | .serverless/ 86 | 87 | # FuseBox cache 88 | .fusebox/ 89 | 90 | # DynamoDB Local files 91 | .dynamodb/ 92 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Pascal Belloncle 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # flexr 2 | A reimplementation of the [Ampleforth](https://www.ampleforth.org/) Token, Oracle and [Geyser](https://www.ampleforth.org/dapps/). 3 | 4 | This also demonstrates how multiple Clarity contracts can interact with each other to provide value to users. The sum is greater than the parts! 5 | 6 | ## TL;DR 7 | [flexr](#the-flexr-token) is an [SRC20 token](#the-src20-token-trait) with an elastic supply, guaranteed by design to be [uncorrelated](#why-non-correlation-is-important) to other tokens. The supply of flexr token gets adjusted ([rebased](#flexr-rebase-math)) on a daily basis based on the price provided by an [Oracle](#the-flexr-oracle). The flexr token can be exchanged with STX on [swapr](#changes-to-swapr). Liquidity providers on swapr are further incentivized by staking the proof they provide liquity on a [Geyser](#changes-to-swapr) that provides higher and higher rewards the longer liquidity providers keep staking. 8 | 9 | ## Introduction 10 | 11 | Like Ampleforth (AMPL), flexr adjusts its supply based on current demand. If the price is lower than the target price, the supply contracts, and in the same fashion, if the price has increased too much, the supply will expand. For long time holders, the daily planned rebase does not affect the amount they own, i.e. holding 2 token worth $1 or holding 1 token worth $2, or 4 tokens worth $0.5 does not change how much your tokens are worth. It does provide an opportunity for short term traders to arbitrage the price around the rebase time, and do so at a [profit](https://gauntlet.network/reports/ampleforth). 12 | 13 | To minimize price movement, the rebase is done with a planned 30 days to reach the target price, with no memory of what was done before, so each rebase adjusts the supply by 1/30 of what is needed based on all available data at the time the rebase occurs. 14 | 15 | Using an elastic supply minimizes the possibility of demand/supply shocks, and creates an asset with near zero correlation to other assets. In other words, this reduces [systematic risk](https://www.ampleforth.org/economics/) 16 | 17 | ### Economic background on flexible synthetic commodities 18 | See the [paper](https://papers.ssrn.com/sol3/papers.cfm?abstract_id=2000118) on Synthetic Commodity Money by George Selgin. 19 | 20 | Abstract 21 | > This paper considers reform possibilities posed by a type of base money that has heretofore been overlooked in the literature on monetary economics. I call this sort of money "synthetic" commodity money because it shares features with both commodity money and fiat money, as these are usually defined, without fitting the conventional definition of either; examples of such money are Bitcoin and the "Swiss dinars" that served as the currency of northern Iraq for over a decade. I argue that the attributes of synthetic commodity money are such as might supply the basis for a monetary regime that does not require oversight by any monetary authority, yet is capable of providing for all such changes in the money stock as may be needed to achieve a high degree of macroeconomic stability. 22 | 23 | 24 | ### More on Ampleforth 25 | The Ampleforth [whitepaper](https://www.ampleforth.org/papers/) 26 | 27 | Abstract 28 | > Synthetic commodities, such as Bitcoin, have thus far demonstrated low correlation with stocks, currencies, and precious metals. However, today’s synthetics are also highly correlated with each other and with Bitcoin. The natural question to ask is: can a synthetic commodity have low correlation with both Bitcoin and traditional asset groups? In this paper, we 1) introduce Ampleforth: a new synthetic commodity and 2) suggest that the Ampleforth protocol, detailed below, will produce a step-function-like volatility fingerprint that is distinct from existing synthetics. 29 | 30 | ### Why non correlation is important 31 | Uncorrelated return streams is the holy grail of portfolio construction, both greatly reducing drawdowns (because one asset zigs when other zags), but potentially providing a total return greater than the average of the weighted returns (because your money has to work less hards as you did not lose as much, i.e. if you lose 50%, you need a gain of 100% to break even). 32 | 33 | ### Example 34 | 35 | Supply, price and balances over time: 36 | | | market cap| total supply |price | alice adjusted balance | alice adjusted share | bob adjusted balance | bob adjusted share | supply∆ | supply∆ / smoothing | 37 | |--|-----------|-----------|------|------------------|----------------|------------------|----------------|--------------|-------------| 38 | |1 | 1,300,000 | 1,000,000 |1.30 | 100,000 | 10.000% | | | 300,000 | 10,000 | 39 | |2 | 1,111,000 | 1,010,000 |1.10 | 101,000 | 10.000% | | | 101,000 | 3,366 | 40 | |3 | 912,030 | 1,013,366 |0.90 | 101,336 | 10.000% | 50,000.0 | 4.934% | -101336 | -3,377 | 41 | |4 | 1,009,988 | 1,009,988 |1 | 100,998 | 10.000% | 49,833.3 | 4.934% | 0 | 0 | 42 | |5 | 1,211,986 | 1,009,988 |1.2 | 100,998 | 10.000% | 49,833.3 | 4.934% | 201,997 | 6,733 | 43 | |6 | 1,118,394 | 1,016,722 |1.1 | 101,672 | 10.000% | 50,165.6 | 4.934% | 101,672 | 3,389 | 44 | 45 | Bob gets 100,000 flexr at t=1 with an adjustment factor of 1,000,000 (the supply at t=1), and Alice gets 50,000 flexr with an ajdustment factor of 1,013,366.667 (the supply at t=3 when she gets her tokens). See below for more on the [adjustement factor](#flexr-rebase-math). 46 | 47 | Note #1: Alice and Bob share remains constant despite the balance and price adjustments. This is the invariant maintained by the rebase. 48 | 49 | Note #2: there is no price change at t=4, so no rebase needed. 50 | 51 | # The flexr ecosystem 52 | 53 | The [flexr token](#the-flexr-token) implements the [SRC20 trait](#the-src20-token-trait) and relies on a new version of [swapr](#changes-to-swapr) for trading. Liquidity providers on [swapr](#changes-to-swapr) can in turn stake their liquidity using a new swapr pair token on the [flexr geyser](#the-flexr-geyser). The longer you stake your liquidity token, the higher the reward you may get (in [flexr](#the-flexr-token) token), up to 3x after 2 weeks (subject to change, as it will likely be longer). The [flexr token](#the-flexr-token) relies on an [Oracle](#the-flexr-oracle) to learn about the price average over the past 24 hours to know whether, and how much to [rebase](#flexr-rebase-math). 54 | 55 | By incentivizing liquidity providers, the Ampleforth token was able to become the pair (AMPL-ETH) with the most liquidity on Uniswap in just a few months. Having a similar incentive should help the flexr token in a similar way. 56 | 57 | 58 | ## The SRC20 token trait 59 | swapr now relies on tokens that implements the [SRC20 trait](./contracts/src20-trait.clar) to allow for: 60 | - transfer 61 | ```closure 62 | (define-public (transfer (recipient principal) (amount uint)) ...) 63 | ``` 64 | - name of the token 65 | ```closure 66 | (define-public (name) ...) 67 | ``` 68 | - getting the balance of an owner 69 | ```closure 70 | (define-public (balance-of (recipient principal)) ...) 71 | ``` 72 | - getting the total supply 73 | ```closure 74 | (define-public (total-supply) ...) 75 | ``` 76 | 77 | SRC20, or Stacks Request for Comments #20 (to keep things simple, as most people understand what [ERC20](https://eips.ethereum.org/EIPS/eip-20) is), although it would be desirable to creat a new SIP so tokens are interoperable on the Stacks chain. SRC20 does not requires the same set of methods ERC20 does. 78 | 79 | ## Changes to swapr 80 | The original version of [swapr](https://github.com/psq/swapr) was relased for the first Blockstak Hackaton. 81 | 82 | In order to support flexr, it was necessary to introduce the following changes: 83 | - balances are no longer hardcoded, allowing for tokens with an elastic supply. It looks like Uniswap v2 can also support this as it supports AMPL. 84 | - the main swapr contract can be used for multiple pairs by leveraging [Clarity Traits](https://docs.blockstack.org/references/clarity-language#dynamic-dispatch), although it is very likely that the final version will need one contract deployed per pair anyway, but this can be the exact same contract, not a contract with hardcoded pair addresses, so a net benefit. 85 | - liquidity providers now receive a [token](./contracts/swapr-token-flexr-wrapr.clar) for their share of liquidity they provide to a pair, allowing them to exchange it with others, or stake it (used by flexr's [geyser](#the-flexr-geyser)!). Uniswap v2 supports this as well. That token itself is also an [SRC20 token](#the-src20-token-trait) with an additional function `mint` only callable by the `swapr` contract when a user adds to their liquidity position). 86 | 87 | Note: technically, the new version of swapr is not part of this Hackaton submission, but shows how various contracts can leverage contracts for a rich ecosystem. The changes, though, are fairly significant. 88 | 89 | ## The flexr token 90 | 91 | The flexr token implements the functions in the [SRC20 trait](#the-src20-token-trait) with the addition of a `rebase` function: 92 | ``` 93 | (define-public (rebase) ...) 94 | ``` 95 | 96 | ### flexr rebase math 97 | During a rebase, everyone's balances get adjusted. As this would not scale very well with a high number of holders, rebase calculate an adjustment factor to apply to each balance, which gets finalized when exchanging the flexr token. `balance-of` also reflects that adjustment. 98 | 99 | ``` 100 | supply∆ = (current_price - price_target) * current_total_supply / price_target 101 | smoothed_supply∆ = supply∆ / 30 102 | ``` 103 | 104 | Each user balance can be calculated by using the current supply, and the supply at the time they got their token, and updating these values each they add or remove tokens as follow: 105 | ``` 106 | user_balance = current_supply * initial_balance / initial_supply 107 | 108 | initial_balance: token balance when position was added 109 | initial_supply: total supply when position was added 110 | ``` 111 | 112 | 113 | Anyone can trigger a rebase, as long as it is past the previous rebase window (about 24 hours), so there is no reliance on an admin to do so. You just pay the minimal transaction fee. 114 | 115 | ## The flexr Oracle 116 | As Clarity does not yet support secp256k1 signature verification (see https://github.com/blockstack/stacks-blockchain/issues/1134, coming up shortly!), it is using a dumb oracle where a trusted party can supply the volume weighted average price for the previous period. That price has to be provided within a small time period before the rebase can be triggered. 117 | 118 | Ultimately, once the support for signature verification is added, the need for an Oracle contract will be removed, and a model like the [Coinbase Oracle](https://docs.pro.coinbase.com/#oracle) can be used by passing a signed price data payload to the [rebase](#flexr-rebase-math) function. The signed payload contains the price and the timestamp, making it secure. Again, removing the need for a trusted third party to update the oracle. Decentralization FTW! Multiple such oracles could be added. 119 | 120 | To avoid rebasing too often and introduce unnecessary noise, a rebase will be triggered only if the price has deviated more than 5% lower or higher than the target price (not fully implemented at the moment). 121 | 122 | ## The flexr Geyser 123 | Liquidity providers on swapr get a token representing their share of the liquity they provide on the flexr-wrapr pair (STX needs to be wrapped to provide an SRC20 equivalent token). Not only do liquitity providers benefit by earning a fee on all swaps, they also benefit by staking their liquity token in exchange for higher and higher rewards the longer they stake their tokens. 124 | 125 | The base reward is calculated as follow: 126 | ``` 127 | reward = amount * reward-factor * #blocks / reward-period per 1000 swapr-flexr-wrapr token in flexr tokens 128 | reward-factor is 1 for first 1000 blocks (about 1 week) 129 | reward-factor is 2 for next 1000 blocks (about 2 weeks) 130 | reward-factor is 3 for anything longer than 2000 blocks (> 2 weeks) 131 | ``` 132 | 133 | The reward gets credited when the user unstakes their liquidity provider tokens (i.e. they gets their liquity token back, and their flexr reward). 134 | 135 | Most likely, the reward periods will be longer (months) to encourage long term holding. 136 | 137 | ## Putting it all together 138 | For more details, see a testing scenario [Scenario description](./scenario.md) or the [tests](./test/unit/flexr.ts) that implement that scenario. As Clarity JS SDK still does not support setting STX balances, this requires a special version of the Clarity JS SDK. Fortunately, `clarity-bin` supports providing a [json](./balances.json) file. 139 | 140 | 141 | # Gotchas 142 | ### Gotcha #1 143 | To run the tests requires an up to date version of the clarity-native-bin module to support setting STX balances from [balances.json](./balances.json) and a patched version of the clarity js sdk. 144 | 145 | See https://github.com/blockstack/clarity-js-sdk/issues/77 for more details. The real fix will need a bit more work than what was used to provide a more developer friendly experience. Happy to provide this patch to anyone, it is only a couple lines. 146 | 147 | ### Gotcha #2 148 | The flexr token can not use the native `ft-token` provided by Clarity, as the `ft-token` does not support dynamically changing the balance as required by the flexr monetary policy. 149 | 150 | This means, however that you lose the safety provided by the post conditions on token transfers. In fact, it would require new types of post conditions (check variable value, check map values, ...), which could be also helpful in other cases. 151 | 152 | ### Gotcha #3 153 | The flexr token relies on `block-height` for defining the rebase window (144 blocks to be exact, which represents about 24 hours with a 10 minutes block average). 154 | 155 | However, as the Clarity JS SDK does not allow for advancing to any block, that value has been artificially reduced (to 3). Having a function to advance to a given block would be highly beneficial to test these kind of use cases that are supposed to be long running (weeks to months, so 1000s of blocks) 156 | 157 | 158 | # Future work 159 | review whether `tx-sender` can be replaced by `contract-caller` in some instances 160 | 161 | -------------------------------------------------------------------------------- /balances.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "principal": "ST3J2GVMMM2R07ZFBJDWTYEYAR8FZH5WKDTFJ9AHA", 4 | "amount": 200000000000000 5 | }, 6 | { 7 | "principal": "SP30JX68J79SMTTN0D2KXQAJBFVYY56BZJEYS3X0B", 8 | "amount": 200000000000000 9 | }, 10 | { 11 | "principal": "SP1EHFWKXQEQD7TW9WWRGSGJFJ52XNGN6MTJ7X462", 12 | "amount": 100000000000000 13 | }, 14 | { 15 | "principal": "SP2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKNRV9EJ7", 16 | "amount": 200000000000 17 | }, 18 | { 19 | "principal": "S02J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKPVKG2CE", 20 | "amount": 100000000000 21 | }, 22 | { 23 | "principal": "SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR", 24 | "amount": 100000000000 25 | }, 26 | { 27 | "principal": "ST3J2GVMMM2R07ZFBJDWTYEYAR8FZH5WKDTFJ9AHA.names", 28 | "amount": 2000 29 | } 30 | ] 31 | -------------------------------------------------------------------------------- /contracts/flexr-stx-token.clar: -------------------------------------------------------------------------------- 1 | ;; we implement the src20 + a mint function 2 | (impl-trait 'ST3J2GVMMM2R07ZFBJDWTYEYAR8FZH5WKDTFJ9AHA.swapr-trait.swapr-trait) 3 | 4 | ;; we can use an ft-token here, so use it! 5 | (define-fungible-token token) 6 | 7 | (define-constant no-acccess-err u30) 8 | 9 | ;; implement all 4 functions required by src20 10 | 11 | (define-public (transfer (recipient principal) (amount uint)) 12 | (begin 13 | (ft-transfer? token amount tx-sender recipient) 14 | ) 15 | ) 16 | 17 | (define-read-only (name) 18 | (contract-call? 'ST3J2GVMMM2R07ZFBJDWTYEYAR8FZH5WKDTFJ9AHA.swapr name 'ST3J2GVMMM2R07ZFBJDWTYEYAR8FZH5WKDTFJ9AHA.flexr-token 'ST3J2GVMMM2R07ZFBJDWTYEYAR8FZH5WKDTFJ9AHA.stx-token) 19 | ) 20 | 21 | (define-read-only (symbol) 22 | (contract-call? 'ST3J2GVMMM2R07ZFBJDWTYEYAR8FZH5WKDTFJ9AHA.swapr symbol 'ST3J2GVMMM2R07ZFBJDWTYEYAR8FZH5WKDTFJ9AHA.flexr-token 'ST3J2GVMMM2R07ZFBJDWTYEYAR8FZH5WKDTFJ9AHA.stx-token) 23 | ) 24 | 25 | ;; the number of decimals used 26 | (define-read-only (decimals) 27 | (ok u6) ;; arbitrary 28 | ) 29 | 30 | (define-read-only (balance-of (owner principal)) 31 | (ok (ft-get-balance token owner)) 32 | ) 33 | 34 | (define-read-only (total-supply) 35 | (contract-call? 'ST3J2GVMMM2R07ZFBJDWTYEYAR8FZH5WKDTFJ9AHA.swapr total-supply 'ST3J2GVMMM2R07ZFBJDWTYEYAR8FZH5WKDTFJ9AHA.flexr-token 'ST3J2GVMMM2R07ZFBJDWTYEYAR8FZH5WKDTFJ9AHA.stx-token) 36 | ) 37 | 38 | ;; the extra mint method used by swapr 39 | ;; can only be used by swapr main contract 40 | (define-public (mint (recipient principal) (amount uint)) 41 | (begin 42 | (print "token-swapr.mint") 43 | (print contract-caller) 44 | (print amount) 45 | (if (is-eq contract-caller 'ST3J2GVMMM2R07ZFBJDWTYEYAR8FZH5WKDTFJ9AHA.swapr) 46 | (ft-mint? token amount recipient) 47 | (err no-acccess-err) 48 | ) 49 | ) 50 | ) 51 | -------------------------------------------------------------------------------- /contracts/flexr-token.clar: -------------------------------------------------------------------------------- 1 | ;; (impl-trait 'ST3J2GVMMM2R07ZFBJDWTYEYAR8FZH5WKDTFJ9AHA.src20-trait.token) 2 | 3 | (define-constant too-soon-err u10) 4 | (define-constant balance-too-low-err u11) 5 | (define-constant err-12 u12) 6 | (define-constant err-13 u13) 7 | 8 | (define-constant target-price 1000000) 9 | (define-constant rebase-lag u10) ;; started with 30 then moved to 10 10 | (define-constant deviation-threshold-high u1050000) 11 | (define-constant deviation-threshold-low u950000) 12 | (define-constant rebalance-period u3) ;; TODO(psq): for testing only, real value should be u144 (1 day) 13 | 14 | (define-data-var supply int (* 1000000000 target-price)) 15 | (define-data-var last-rebase-height uint u0) 16 | 17 | (define-map balances 18 | { owner: principal } 19 | { base-amount: uint, total-supply-adjuster: uint } 20 | ) 21 | 22 | (define-private (balance-set (recipient principal) (amount uint)) 23 | (begin 24 | (print "flexr.balance-set") 25 | (print recipient) 26 | (print amount) 27 | (print (var-get supply)) 28 | (print (map-set balances {owner: recipient} {base-amount: amount, total-supply-adjuster: (to-uint (var-get supply))})) 29 | ) 30 | ) 31 | 32 | (define-public (transfer (recipient principal) (amount uint)) 33 | (let ((balance-sender (unwrap-panic (balance-of tx-sender))) (balance-recipient (unwrap-panic (balance-of recipient)))) 34 | (print "flexr.transfer") 35 | (print tx-sender) 36 | (print contract-caller) 37 | (print recipient) 38 | (print amount) 39 | (print "balance-sender") 40 | (print balance-sender) 41 | (print "balance-recipient") 42 | (print balance-recipient) 43 | (if (>= balance-sender amount) 44 | (begin 45 | (balance-set tx-sender (- balance-sender amount)) 46 | (balance-set recipient (+ balance-recipient amount)) 47 | (ok true) 48 | ) 49 | (begin 50 | (err balance-too-low-err) 51 | ) 52 | ) 53 | ) 54 | ) 55 | 56 | ;; returns the balance of `recipient` 57 | ;; total-supply * base-amount / total-supply-adjuster 58 | (define-public (balance-of (recipient principal)) 59 | (let ((balance (map-get? balances {owner: recipient}))) 60 | (if (is-some balance) 61 | (ok (/ (* (to-uint (var-get supply)) (unwrap-panic (get base-amount balance))) (unwrap-panic (get total-supply-adjuster balance)))) 62 | (ok u0) 63 | ) 64 | ) 65 | ) 66 | 67 | ;; ;; returns the total number of tokens 68 | (define-read-only (total-supply) 69 | (ok (to-uint (var-get supply))) 70 | ) 71 | 72 | ;; returns the token name 73 | (define-read-only (name) 74 | (ok "flexr") 75 | ) 76 | 77 | ;; the token symbol 78 | (define-read-only (symbol) 79 | (ok "FLXR") 80 | ) 81 | 82 | ;; the number of decimals used 83 | (define-read-only (decimals) 84 | (ok u6) 85 | ) 86 | 87 | 88 | ;; can be run by anyone as long as the block height is farther enough from the previous rebase 89 | (define-public (rebase) 90 | (begin 91 | (if (> block-height (+ (var-get last-rebase-height) rebalance-period)) 92 | (begin 93 | (var-set last-rebase-height block-height) ;; TODO(psq): round down so rebase period is actually === rebalance-period? 94 | ;; TODO(psq): only call run-rebase if outside deviation-threshold-low - deviation-threshold-high 95 | (ok (run-rebase)) 96 | ) 97 | (err too-soon-err) 98 | ) 99 | ) 100 | ) 101 | 102 | ;; internal calculation of the smoothed supply adjustment 103 | (define-private (run-rebase) 104 | (let ((current-price-data (contract-call? 'ST3J2GVMMM2R07ZFBJDWTYEYAR8FZH5WKDTFJ9AHA.oracle get-price))) 105 | (let ((current-price (get price current-price-data))) 106 | (let ((supply-delta (/ (* (- (to-int current-price) target-price) (var-get supply)) target-price))) 107 | (let ((supply-delta-smoothed (/ supply-delta (to-int rebase-lag)))) 108 | (var-set supply (+ (var-get supply) supply-delta-smoothed)) 109 | ) 110 | ) 111 | ) 112 | ) 113 | ) 114 | 115 | ;; fund the flexr treasury 116 | (map-set balances {owner: 'SP30JX68J79SMTTN0D2KXQAJBFVYY56BZJEYS3X0B} {base-amount: u950000000000000, total-supply-adjuster: u1000000000000000}) 117 | ;; swapr test address 118 | (map-set balances {owner: 'ST3J2GVMMM2R07ZFBJDWTYEYAR8FZH5WKDTFJ9AHA} {base-amount: u100000000000000, total-supply-adjuster: u1000000000000000}) 119 | ;; psq test address 120 | (map-set balances {owner: 'ST1TWA18TSWGDAFZT377THRQQ451D1MSEM69C761} {base-amount: u1000000000000, total-supply-adjuster: u1000000000000000}) 121 | (map-set balances {owner: 'ST50GEWRE7W5B02G3J3K19GNDDAPC3XPZPYQRQDW} {base-amount: u1000000000000, total-supply-adjuster: u1000000000000000}) 122 | (map-set balances {owner: 'ST2SVRCJJD90TER037VCSAFA781HQTCPFK9YRA6J5} {base-amount: u1000000000000, total-supply-adjuster: u1000000000000000}) 123 | ;; fund flexr geyser 124 | (map-set balances {owner: 'ST3J2GVMMM2R07ZFBJDWTYEYAR8FZH5WKDTFJ9AHA.geyser} {base-amount: u50000000000000, total-supply-adjuster: u1000000000000000}) 125 | 126 | -------------------------------------------------------------------------------- /contracts/geyser.clar: -------------------------------------------------------------------------------- 1 | (define-constant swapr-transfer-failed-err u42) 2 | (define-constant reward-transfer-failed-err u43) 3 | (define-constant no-stake-err u44) 4 | (define-constant transfer-fail-err u45) 5 | (define-constant geyser-empty-err u46) 6 | 7 | (define-constant reward-period u1000) 8 | (define-constant reward-period-1 u1000) 9 | (define-constant reward-period-2 u2000) 10 | 11 | (define-constant reward1 u1) 12 | (define-constant reward2 u2) 13 | (define-constant reward3 u3) 14 | 15 | (define-map balances 16 | { owner: principal } 17 | { 18 | amount: uint, 19 | height: uint, 20 | } 21 | ) 22 | 23 | (define-data-var supply uint u0) 24 | 25 | ;; stake token 26 | ;; pass in a swapr token for the FLEXR-STX pair token and amount 27 | (define-public (stake (amount uint)) 28 | (begin 29 | (print "geyser.stake") 30 | (print amount) 31 | (if (is-ok (contract-call? 'ST3J2GVMMM2R07ZFBJDWTYEYAR8FZH5WKDTFJ9AHA.flexr-stx-token transfer (as-contract tx-sender) amount)) 32 | (let ((prior-amount (default-to u0 (get amount (map-get? balances {owner: tx-sender}))))) 33 | (print prior-amount) 34 | (map-set balances {owner: tx-sender} {amount: (+ amount prior-amount), height: block-height}) 35 | (var-set supply (+ (var-get supply) amount)) 36 | (ok true) 37 | ) 38 | (err transfer-fail-err) 39 | ) 40 | ) 41 | ) 42 | 43 | 44 | ;; unstake token 45 | ;; collect back the FLEXR-STX pair token and FLEXR reward 46 | ;; reward is based on number of block token was staked (1000 blocks => x1, 2000 blocks => 2x, 3000 blocks => 3x) 47 | (define-public (unstake) 48 | (let ((balance (map-get? balances {owner: tx-sender}))) 49 | (if (is-some balance) 50 | (let ((amount (unwrap-panic (get amount balance))) (height (unwrap-panic (get height balance)))) 51 | (var-set supply (- (var-get supply) amount)) 52 | (let ((blocks (- block-height height))) 53 | (if (< blocks reward-period-1) 54 | (send-back tx-sender amount blocks reward1) 55 | (if (< blocks reward-period-2) 56 | (send-back tx-sender amount blocks reward2) 57 | (send-back tx-sender amount blocks reward3) 58 | ) 59 | ) 60 | ) 61 | ;; (ok true) 62 | ) 63 | (err no-stake-err) 64 | ) 65 | ) 66 | ) 67 | 68 | ;; reward is amount * reward-factor * #blocks / reward-period per 1000 swapr-flexr-wrapr token in flexr tokens 69 | (define-private (send-back (recipient principal) (amount uint) (blocks uint) (reward-factor uint)) 70 | (let ((reward-amount (/ (* amount (* blocks reward-factor)) (* u1000 reward-period)))) 71 | (print "geyser.send-back") 72 | (print recipient) 73 | (print amount) 74 | (print blocks) 75 | (print reward-factor) 76 | (print reward-amount) 77 | (print (as-contract tx-sender)) 78 | (if (is-ok (as-contract (contract-call? 'ST3J2GVMMM2R07ZFBJDWTYEYAR8FZH5WKDTFJ9AHA.flexr-stx-token transfer recipient amount))) 79 | (if (is-ok (as-contract (contract-call? 'ST3J2GVMMM2R07ZFBJDWTYEYAR8FZH5WKDTFJ9AHA.flexr-token transfer recipient reward-amount))) 80 | (ok true) 81 | (err reward-transfer-failed-err) 82 | ) 83 | (err swapr-transfer-failed-err) 84 | ) 85 | ) 86 | ) 87 | 88 | ;; find out total amount of staked FLEXR-WRAPR pair tokens 89 | (define-read-only (total-supply) 90 | (ok (var-get supply)) 91 | ) 92 | -------------------------------------------------------------------------------- /contracts/oracle.clar: -------------------------------------------------------------------------------- 1 | (define-constant err-not-white-listed u51) 2 | 3 | (define-data-var last-price uint u0) 4 | (define-data-var last-block uint u0) 5 | ;; (define-data-var price uint u0) 6 | 7 | (define-constant oracle-owner 'SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR) 8 | 9 | 10 | ;; for now, only allow owner, but eventually: 11 | ;; 12 | ;; Can be called by anyone using a vetted oracle that provides signed volume weighted price data for the past 24 hours 13 | ;; using stubbed calls till secp256k1 verification is implemented 14 | ;; will consider rewarding sender with some flexr tokens 15 | ;; data needs to be sent 10-15 blocks before next rebalance, picks a winner at random 16 | ;; or rather, the signed payload can be passed to flexr.rebase, removing the need for a separate oracle contract, and 1 less transaction per day 17 | (define-public (update-price (price uint)) 18 | (if (is-eq tx-sender oracle-owner) 19 | (begin 20 | (var-set last-price price) 21 | (var-set last-block block-height) 22 | (ok true) 23 | ) 24 | (err err-not-white-listed) 25 | ) 26 | ) 27 | 28 | ;; get the latest price within 10-15 blocks of rebalance, or no rebalance happens 29 | (define-read-only (get-price) 30 | { price: (var-get last-price), height: (var-get last-block)} 31 | ) 32 | 33 | ;; TODO(psq): use the same format for messages as coinbase oracle with secp256k1 signatures -------------------------------------------------------------------------------- /contracts/plaid-stx-token.clar: -------------------------------------------------------------------------------- 1 | ;; ;; we implement the sip-010 + a mint function 2 | (impl-trait 'ST000000000000000000002AMW42H.swapr-trait.swapr-trait) 3 | 4 | ;; ;; we can use an ft-token here, so use it! 5 | (define-fungible-token token) 6 | 7 | (define-constant no-acccess-err u40) 8 | 9 | ;; implement all functions required by sip-010 10 | 11 | (define-public (transfer (amount uint) (sender principal) (recipient principal)) 12 | (begin 13 | (ft-transfer? token amount tx-sender recipient) 14 | ) 15 | ) 16 | 17 | (define-read-only (get-name) 18 | (ok "plaid-stx-swapr") 19 | ;; (contract-call? 'ST000000000000000000002AMW42H.swapr get-name 'ST000000000000000000002AMW42H.plaid-token 'ST000000000000000000002AMW42H.stx-token) 20 | ) 21 | 22 | (define-read-only (get-symbol) 23 | (ok "plaid-stx-swapr") 24 | ;; (contract-call? 'ST000000000000000000002AMW42H.swapr get-symbol 'ST000000000000000000002AMW42H.plaid-token 'ST000000000000000000002AMW42H.stx-token) 25 | ) 26 | 27 | ;; the number of decimals used 28 | (define-read-only (get-decimals) 29 | (ok u6) ;; arbitrary, or ok? 30 | ) 31 | 32 | (define-read-only (get-balance-of (owner principal)) 33 | (ok (ft-get-balance token owner)) 34 | ) 35 | 36 | (define-read-only (get-total-supply) 37 | (ok u0) 38 | ;; (contract-call? 'ST000000000000000000002AMW42H.swapr get-total-supply 'ST000000000000000000002AMW42H.plaid-token 'ST000000000000000000002AMW42H.stx-token) 39 | ) 40 | 41 | (define-read-only (get-token-uri) 42 | (ok (some u"https://swapr.finance/tokens/plaid-stx-token.json")) 43 | ) 44 | 45 | 46 | ;; the extra mint method used by swapr 47 | ;; can only be used by swapr main contract 48 | (define-public (mint (recipient principal) (amount uint)) 49 | (begin 50 | (print "token-swapr.mint") 51 | (print contract-caller) 52 | (print amount) 53 | (if (is-eq contract-caller 'ST000000000000000000002AMW42H.swapr) 54 | (ft-mint? token amount recipient) 55 | (err no-acccess-err) 56 | ) 57 | ) 58 | ) 59 | -------------------------------------------------------------------------------- /contracts/plaid-token.clar: -------------------------------------------------------------------------------- 1 | ;; wrap the native STX token into an SRC20 compatible token to be usable along other tokens 2 | ;; (use-trait src20-token .src20-trait.src20-trait) 3 | (impl-trait 'ST000000000000000000002AMW42H.sip-010.ft-trait) 4 | 5 | (define-fungible-token plaid) 6 | 7 | ;; get the token balance of owner 8 | (define-read-only (get-balance-of (owner principal)) 9 | (ok (ft-get-balance plaid owner)) 10 | ) 11 | 12 | ;; returns the total number of tokens 13 | ;; TODO(psq): we don't have access yet, but once POX is available, this should be a value that 14 | ;; is available from Clarity 15 | (define-read-only (get-total-supply) 16 | (ok u0) 17 | ) 18 | 19 | ;; returns the token name 20 | (define-read-only (get-name) 21 | (ok "Plaid") 22 | ) 23 | 24 | (define-read-only (get-symbol) 25 | (ok "PLD") 26 | ) 27 | 28 | ;; the number of decimals used 29 | (define-read-only (get-decimals) 30 | (ok u8) 31 | ) 32 | 33 | (define-read-only (get-token-uri) 34 | (ok (some u"https://swapr.finance/tokens/plaid.json")) 35 | ) 36 | ;; { 37 | ;; "name":"Plaid", 38 | ;; "description":"Plaid token, uses as a test token", 39 | ;; "image":"https://swapr.finance/tokens/plaid.png" 40 | ;; } 41 | 42 | 43 | ;; (transfer (uint principal principal) (response bool uint)) 44 | ;; amount sender recipient 45 | ;; Transfers tokens to a recipient 46 | (define-public (transfer (amount uint) (sender principal) (recipient principal)) 47 | (begin 48 | (print "plaid.transfer") 49 | (print amount) 50 | (print tx-sender) 51 | (print recipient) 52 | (asserts! (is-eq tx-sender sender) (err u255)) ;; too strict? 53 | (print (ft-transfer? plaid amount tx-sender recipient)) 54 | ) 55 | ) 56 | 57 | ;; (ft-mint? plaid u100000000000000 'ST3J2GVMMM2R07ZFBJDWTYEYAR8FZH5WKDTFJ9AHA) 58 | ;; (ft-mint? plaid u100000000000000 'ST1TWA18TSWGDAFZT377THRQQ451D1MSEM69C761) 59 | ;; (ft-mint? plaid u100000000000000 'ST50GEWRE7W5B02G3J3K19GNDDAPC3XPZPYQRQDW) 60 | (ft-mint? plaid u1000000000000 'ST2SVRCJJD90TER037VCSAFA781HQTCPFK9YRA6J5) ;; expected but clarinet uses ST3AA33M8SS15A30ETXE134ZXD8TNEDHT8Q955G40 61 | (ft-mint? plaid u1000000000000 'ST3AA33M8SS15A30ETXE134ZXD8TNEDHT8Q955G40) ;; for clarinet, because not the same derivation 62 | -------------------------------------------------------------------------------- /contracts/src20-trait.clar: -------------------------------------------------------------------------------- 1 | ;; Time for a Stacks Request for Comment, the 20th edition... 2 | ;; The function needed by tokens compatible with swapr, and the geyser 3 | ;; a subset of ERC20 methods 4 | (define-trait src20-trait 5 | ( 6 | ;; Transfer from the caller to a new principal 7 | (transfer (principal uint) (response bool uint)) 8 | 9 | ;; the human readable name of the token 10 | (name () (response (string-ascii 32) uint)) 11 | 12 | ;; the ticker symbol, or empty if none 13 | (symbol () (response (string-ascii 32) uint)) 14 | 15 | ;; the number of decimals used, e.g. 6 would mean 1_000_000 represents 1 token 16 | (decimals () (response uint uint)) 17 | 18 | ;; the balance of the passed principal 19 | (balance-of (principal) (response uint uint)) 20 | 21 | ;; the current total supply (which does not need to be a constant) 22 | (total-supply () (response uint uint)) 23 | ) 24 | ) 25 | -------------------------------------------------------------------------------- /contracts/stx-token.clar: -------------------------------------------------------------------------------- 1 | ;; wrap the native STX token into an SRC20 compatible token to be usable along other tokens 2 | ;; (use-trait src20-token .src20-trait.src20-trait) 3 | (impl-trait 'ST000000000000000000002AMW42H.sip-010.ft-trait) 4 | 5 | ;; get the token balance of owner 6 | (define-read-only (get-balance-of (owner principal)) 7 | (begin 8 | (ok (print (stx-get-balance owner))) 9 | ) 10 | ) 11 | 12 | ;; returns the total number of tokens 13 | ;; TODO(psq): we don't have access yet, but once POX is available, this should be a value that 14 | ;; is available from Clarity 15 | (define-read-only (get-total-supply) 16 | (ok u0) 17 | ) 18 | 19 | ;; returns the token name 20 | (define-read-only (get-name) 21 | (ok "stx") 22 | ) 23 | 24 | (define-read-only (get-symbol) 25 | (ok "STX") 26 | ) 27 | 28 | ;; the number of decimals used 29 | (define-read-only (get-decimals) 30 | (ok u6) 31 | ) 32 | 33 | (define-read-only (get-token-uri) 34 | (ok (some u"https://swapr.finance/tokens/stx.json")) 35 | ) 36 | ;; { 37 | ;; "name":"STX", 38 | ;; "description":"STX token, as a SIP-010 compatible token", 39 | ;; "image":"https://swapr.finance/tokens/stx.png" 40 | ;; } 41 | 42 | ;; Transfers tokens to a recipient 43 | (define-public (transfer (amount uint) (sender principal) (recipient principal)) 44 | (begin 45 | (print "stx.transfer") 46 | (print amount) 47 | (print tx-sender) 48 | (print recipient) 49 | (asserts! (is-eq tx-sender sender) (err u255)) ;; too strict? 50 | (print (stx-transfer? amount tx-sender recipient)) 51 | ) 52 | ) 53 | -------------------------------------------------------------------------------- /contracts/swapr-trait.clar: -------------------------------------------------------------------------------- 1 | ;; this is an SRC20 method with an additional mint function 2 | ;; as Clarity does not support "includes", copy the needed funcitons, and add new ones 3 | 4 | (define-trait swapr-trait 5 | ( 6 | (transfer (principal uint) (response bool uint)) 7 | (name () (response (string-ascii 32) uint)) 8 | (symbol () (response (string-ascii 32) uint)) 9 | (decimals () (response uint uint)) 10 | (balance-of (principal) (response uint uint)) 11 | (total-supply () (response uint uint)) 12 | (mint (principal uint) (response bool uint)) 13 | ) 14 | ) 15 | -------------------------------------------------------------------------------- /contracts/swapr.clar: -------------------------------------------------------------------------------- 1 | (use-trait src20-token 'ST000000000000000000002AMW42H.sip-010.ft-trait) 2 | (use-trait swapr-token 'ST000000000000000000002AMW42H.swapr-trait.swapr-trait) 3 | 4 | (define-constant contract-owner 'ST3J2GVMMM2R07ZFBJDWTYEYAR8FZH5WKDTFJ9AHA) 5 | (define-constant no-liquidity-err (err u61)) 6 | (define-constant transfer-failed-err (err u62)) 7 | (define-constant not-owner-err (err u63)) 8 | (define-constant no-fee-to-address-err (err u64)) 9 | (define-constant invalid-pair-err (err u65)) 10 | (define-constant no-such-position-err (err u66)) 11 | (define-constant balance-too-low-err (err u67)) 12 | (define-constant too-many-pairs-err (err u68)) 13 | (define-constant pair-already-exists-err (err u69)) 14 | (define-constant wrong-token-err (err u70)) 15 | (define-constant too-much-slippage-err (err u71)) 16 | 17 | ;; for future use, or debug 18 | (define-constant e10-err (err u20)) 19 | (define-constant e11-err (err u21)) 20 | (define-constant e12-err (err u22)) 21 | 22 | ;; ;; V1 23 | ;; ;; overall balance of x-token and y-token held by the contract 24 | ;; (define-data-var balance-x uint u0) 25 | ;; (define-data-var balance-y uint u0) 26 | 27 | ;; ;; fees collected so far, that have not been withdrawn (saves gas while doing exchanges) 28 | ;; (define-data-var fee-balance-x uint u0) 29 | ;; (define-data-var fee-balance-y uint u0) 30 | 31 | ;; ;; balances help by all the clients holding shares, this is equal to the sum of all the balances held in shares by each client 32 | ;; (define-data-var total-balances uint u0) 33 | ;; (define-map shares 34 | ;; ((owner principal)) 35 | ;; ((balance uint)) 36 | ;; ) 37 | 38 | ;; ;; when set, enables the fee, and provides whene to send the fee when calling collect-fees 39 | ;; (define-data-var fee-to-address (optional principal) none) 40 | 41 | ;; V2 42 | ;; variables 43 | ;; (name) => (token-x, token-y) 44 | ;; (token-x, token-y) => (shares-total, balance-x, balance-y, fee-balance-x, fee-balance-y, fee-to-address) 45 | ;; (token-x, token-y, principal) => (shares) 46 | 47 | (define-map pairs-map 48 | { pair-id: uint } 49 | { 50 | token-x: principal, 51 | token-y: principal, 52 | } 53 | ) 54 | 55 | (define-map pairs-data-map 56 | { 57 | token-x: principal, 58 | token-y: principal, 59 | } 60 | { 61 | shares-total: uint, 62 | fee-balance-x: uint, 63 | fee-balance-y: uint, 64 | fee-to-address: (optional principal), 65 | swapr-token: principal, 66 | name: (string-ascii 32), 67 | } 68 | ) 69 | 70 | ;; ;; TODO(psq): replace use of balance-x/balance-y with a call to balance-of(swapr) on the token itself, no write to do actually!!! The transfer is the write, that's cool :) 71 | 72 | 73 | ;; (define-map shares-map 74 | ;; ((token-x principal) (token-y principal) (owner principal)) 75 | ;; ((shares uint)) 76 | ;; ) 77 | 78 | ;; (define-data-var pairs-list (list 2000 uint) (list)) 79 | (define-data-var pair-count uint u0) 80 | 81 | 82 | ;; token support, these ~~4~~3 calls need to be wrapped in a contract that hardcodes the 2 pairs to present 83 | ;; a unified, src20 compatible trait 84 | 85 | ;; (define-public (transfer (token-x-trait ) (token-y-trait ) (recipient principal) (amount uint) (token-trait )) 86 | ;; (let ((token-x (contract-of token-x-trait)) (token-y (contract-of token-y-trait)) (token (contract-of token-trait))) 87 | ;; (let ((pair (unwrap! (map-get? pairs-data-map { token-x: token-x, token-y: token-y }) invalid-pair-err))) 88 | ;; (if (eq token (get swapr-token pair)) 89 | ;; (contract-call? token-trait transfer recipient amount) 90 | ;; (err wrong-token-err) 91 | ;; ) 92 | ;; ) 93 | ;; ) 94 | ;; ) 95 | 96 | (define-read-only (get-name (token-x-trait ) (token-y-trait )) 97 | (let ((token-x (contract-of token-x-trait)) (token-y (contract-of token-y-trait))) 98 | (let ((pair (unwrap! (map-get? pairs-data-map { token-x: token-x, token-y: token-y }) invalid-pair-err))) 99 | (ok (get name pair)) 100 | ) 101 | ) 102 | ) 103 | 104 | (define-public (get-symbol (token-x-trait ) (token-y-trait )) 105 | ;; TODO(psq): this should be the symbol of the pair, not a single token 106 | ;; TODO(psq): obvious not the rigth thing to do here 107 | ;; (contract-call? 'ST3J2GVMMM2R07ZFBJDWTYEYAR8FZH5WKDTFJ9AHA.plaid-token symbol) 108 | (contract-call? token-y-trait get-symbol) 109 | ;; (ok (concat (unwrap-panic (as-max-len? (unwrap-panic (contract-call? token-x-trait symbol)) u15)) (concat "-" (unwrap-panic (as-max-len? (unwrap-panic (contract-call? token-y-trait symbol)) u15))))) 110 | ) 111 | 112 | ;; (define-public (balance-of (token-x-trait ) (token-y-trait ) (owner principal)) 113 | ;; (begin 114 | ;; (let ((token-x (contract-of token-x-trait)) (token-y (contract-of token-y-trait))) 115 | ;; (print "swapr.balance-of") 116 | ;; (print token-x) 117 | ;; (print token-y) 118 | ;; (print owner) 119 | ;; (ok (print (shares-of token-x token-y owner))) 120 | ;; ) 121 | ;; ) 122 | ;; ) 123 | 124 | (define-read-only (get-total-supply (token-x-trait ) (token-y-trait )) 125 | (let ((token-x (contract-of token-x-trait)) (token-y (contract-of token-y-trait))) 126 | (let ((pair (unwrap! (map-get? pairs-data-map { token-x: token-x, token-y: token-y }) invalid-pair-err))) 127 | (ok (get shares-total pair)) 128 | ) 129 | ) 130 | ) 131 | 132 | 133 | ;; wrappers to get an owner's position 134 | ;; (define-private (shares-of (token-x principal) (token-y principal) (owner principal)) 135 | ;; (default-to u0 136 | ;; (get shares 137 | ;; (map-get? shares-map ((token-x token-x) (token-y token-y) (owner owner))) 138 | ;; ) 139 | ;; ) 140 | ;; ) 141 | 142 | ;; get the number of shares of the pool for owner 143 | ;; (define-read-only (get-shares-of (token-x principal) (token-y principal) (owner principal)) 144 | ;; (ok (shares-of token-x token-y owner)) 145 | ;; ) 146 | 147 | ;; get the total number of shares in the pool 148 | (define-read-only (get-shares (token-x principal) (token-y principal)) 149 | (ok (get shares-total (unwrap! (map-get? pairs-data-map { token-x: token-x, token-y: token-y }) invalid-pair-err))) 150 | ) 151 | 152 | ;; TODO(psq): only works if a token is in only one pair, to make this work, needs a new instance of the contract per pair at a different address 153 | ;; which would remove the need for a separate token, as swapr can be its own token 154 | (define-private (get-balance (token-trait )) 155 | (begin 156 | (unwrap-panic (contract-call? token-trait get-balance-of (as-contract tx-sender))) 157 | ) 158 | ) 159 | 160 | ;; (define-public (get-balances-of (token-x-trait ) (token-y-trait ) (owner principal)) 161 | ;; (let ((token-x (contract-of token-x-trait)) (token-y (contract-of token-y-trait))) 162 | ;; (let ((pair (unwrap! (map-get? pairs-data-map { token-x: token-x, token-y: token-y }) invalid-pair-err))) 163 | ;; (let ((x (balance token-x-trait)) (y (balance token-y-trait)) (shares-total (get shares-total pair)) (shares (shares-of token-x token-y owner))) 164 | ;; (if (> shares-total u0) 165 | ;; (ok (list (/ (* x shares) shares-total) (/ (* y shares) shares-total))) ;; less precision loss doing * first 166 | ;; no-liquidity-err ;; no liquidity 167 | ;; ) 168 | ;; ) 169 | ;; ) 170 | ;; ) 171 | ;; ) 172 | 173 | ;; (define-private (increase-shares (token-x principal) (token-y principal) (owner principal) (amount uint)) 174 | ;; (let ((shares (shares-of token-x token-y owner))) 175 | ;; (print "swapr.increase-shares") 176 | ;; (print token-x) 177 | ;; (print token-y) 178 | ;; (print owner) 179 | ;; (print shares) 180 | ;; (print amount) 181 | ;; (print (map-set shares-map 182 | ;; ((token-x token-x) (token-y token-y) (owner owner)) 183 | ;; ((shares (+ shares amount))) 184 | ;; )) 185 | ;; (ok true) 186 | ;; ) 187 | ;; ) 188 | 189 | ;; (define-private (decrease-shares (token-x principal) (token-y principal) (owner principal) (amount uint)) 190 | ;; (let ((shares (shares-of token-x token-y owner))) 191 | ;; (if (< amount shares) 192 | ;; (begin 193 | ;; (map-set shares-map 194 | ;; ((token-x token-x) (token-y token-y) (owner owner)) 195 | ;; ((shares (- shares amount))) 196 | ;; ) 197 | ;; (ok true) 198 | ;; ) 199 | ;; balance-too-low-err 200 | ;; ) 201 | ;; ) 202 | ;; ) 203 | 204 | ;; get overall balances for the pair 205 | (define-public (get-balances (token-x-trait ) (token-y-trait )) 206 | (let ((token-x (contract-of token-x-trait)) (token-y (contract-of token-y-trait))) 207 | (let ((pair (unwrap! (map-get? pairs-data-map { token-x: token-x, token-y: token-y }) invalid-pair-err))) 208 | (ok (list (get-balance token-x-trait) (get-balance token-y-trait))) 209 | ) 210 | ) 211 | ) 212 | 213 | ;; since we can't use a constant to refer to contract address, here what x and y are 214 | ;; (define-constant x-token 'SP2NC4YKZWM2YMCJV851VF278H9J50ZSNM33P3JM1.my-token) 215 | ;; (define-constant y-token 'SP1QR3RAGH3GEME9WV7XB0TZCX6D5MNDQP97D35EH.my-token) 216 | (define-public (add-to-position (token-x-trait ) (token-y-trait ) (token-swapr-trait ) (x uint) (y uint)) 217 | (let ((token-x (contract-of token-x-trait)) (token-y (contract-of token-y-trait))) 218 | (let ((pair (unwrap! (map-get? pairs-data-map { token-x: token-x, token-y: token-y }) invalid-pair-err)) (contract-address (as-contract tx-sender)) (recipient-address tx-sender)) 219 | ;; TODO(psq) check if x or y is 0, to calculate proper exchange rate unless shares-total is 0, which would be an error 220 | (if 221 | (and 222 | ;; TODO(psq): check that the amount transfered in matches the amount requested 223 | (is-ok (contract-call? token-x-trait transfer x tx-sender contract-address)) 224 | (is-ok (contract-call? token-y-trait transfer y tx-sender contract-address)) 225 | ) 226 | (begin 227 | (print "calculate new-shares") 228 | (let ((new-shares (if (is-eq (print (get shares-total pair)) u0) 229 | (let ((shares (sqrti (* x y)))) 230 | ;; (increase-shares token-x token-y tx-sender shares) 231 | shares 232 | ) 233 | (let ((shares (/ (* (print x) (print (get shares-total pair))) (print (get-balance token-x-trait))))) 234 | ;; (increase-shares token-x token-y tx-sender shares) 235 | shares 236 | ) 237 | ))) 238 | (map-set pairs-data-map { token-x: token-x, token-y: token-y } 239 | { 240 | shares-total: (+ new-shares (get shares-total pair)), 241 | fee-balance-x: (get fee-balance-x pair), 242 | fee-balance-y: (get fee-balance-y pair), 243 | fee-to-address: (get fee-to-address pair), 244 | name: (get name pair), 245 | swapr-token: (get swapr-token pair), 246 | } 247 | ) 248 | (print "token-swapr-trait.mint params") 249 | (print x) 250 | (print y) 251 | (print recipient-address) 252 | (print new-shares) 253 | (print (contract-call? token-swapr-trait mint recipient-address new-shares)) 254 | ) 255 | ) 256 | (begin 257 | transfer-failed-err 258 | ) 259 | ) 260 | ) 261 | ) 262 | ) 263 | 264 | (define-read-only (get-pair-details (token-x principal) (token-y principal)) 265 | (unwrap-panic (map-get? pairs-data-map { token-x: token-x, token-y: token-y })) 266 | ) 267 | 268 | (define-read-only (get-pair-contracts (pair-id uint)) 269 | (unwrap-panic (map-get? pairs-map { pair-id: pair-id })) 270 | ) 271 | 272 | (define-read-only (get-pair-count) 273 | (ok (var-get pair-count)) 274 | ) 275 | 276 | ;; (define-read-only (get-pairs) 277 | ;; (ok (map get-pair-contracts (var-get pairs-list))) 278 | ;; ) 279 | 280 | (define-public (create-pair (token-x-trait ) (token-y-trait ) (token-swapr-trait ) (pair-name (string-ascii 32)) (x uint) (y uint)) 281 | ;; TOOD(psq): add creation checks, then create map before proceeding to add-to-position 282 | ;; check neither x,y or y,x exists` 283 | (let ((name-x (unwrap-panic (contract-call? token-x-trait get-name))) (name-y (unwrap-panic (contract-call? token-y-trait get-name)))) 284 | (let ((token-x (contract-of token-x-trait)) (token-y (contract-of token-y-trait)) (pair-id (+ (var-get pair-count) u1))) 285 | (if (and (is-none (map-get? pairs-data-map { token-x: token-x, token-y: token-y })) (is-none (map-get? pairs-data-map { token-x: token-y, token-y: token-x }))) 286 | (begin 287 | (map-set pairs-data-map { token-x: token-x, token-y: token-y } 288 | { 289 | shares-total: u0, 290 | fee-balance-x: u0, 291 | fee-balance-y: u0, 292 | fee-to-address: none, 293 | swapr-token: (contract-of token-swapr-trait), 294 | name: pair-name, 295 | } 296 | ) 297 | (map-set pairs-map { pair-id: pair-id } { token-x: token-x, token-y: token-y }) 298 | ;; (var-set pairs-list (unwrap! (as-max-len? (append (var-get pairs-list) pair-id) u2000) too-many-pairs-err)) 299 | (var-set pair-count pair-id) 300 | (add-to-position token-x-trait token-y-trait token-swapr-trait x y) 301 | ) 302 | pair-already-exists-err 303 | ) 304 | ) 305 | ) 306 | ) 307 | 308 | 309 | 310 | ;; ;; reduce the amount of liquidity the sender provides to the pool 311 | ;; ;; to close, use u100 312 | (define-public (reduce-position (token-x-trait ) (token-y-trait ) (token-swapr-trait ) (percent uint)) 313 | (let ((token-x (contract-of token-x-trait)) (token-y (contract-of token-y-trait))) 314 | (let ((pair (unwrap! (map-get? pairs-data-map { token-x: token-x, token-y: token-y }) invalid-pair-err))) 315 | (let ((shares (unwrap-panic (contract-call? token-swapr-trait get-balance-of tx-sender))) (shares-total (get shares-total pair)) (contract-address (as-contract tx-sender)) (sender tx-sender)) 316 | (let ((withdrawal (/ (* shares percent) u100))) 317 | (let ((withdrawal-x (/ (* withdrawal (get-balance token-x-trait)) shares-total)) (withdrawal-y (/ (* withdrawal (get-balance token-y-trait)) shares-total))) 318 | (if 319 | (and 320 | (<= percent u100) 321 | (is-ok (as-contract (contract-call? token-x-trait transfer withdrawal-x contract-address sender))) 322 | (is-ok (as-contract (contract-call? token-y-trait transfer withdrawal-y contract-address sender))) 323 | ) 324 | (begin 325 | ;; (unwrap-panic (decrease-shares token-x token-y tx-sender withdrawal)) ;; should never fail, you know... 326 | (map-set pairs-data-map { token-x: token-x, token-y: token-y } 327 | { 328 | shares-total: (- shares-total withdrawal), 329 | fee-balance-x: (get fee-balance-x pair), 330 | fee-balance-y: (get fee-balance-y pair), 331 | fee-to-address: (get fee-to-address pair), 332 | name: (get name pair), 333 | swapr-token: (get swapr-token pair), 334 | } 335 | ) 336 | ;; TODO(psq): use burn 337 | (unwrap-panic (contract-call? token-swapr-trait transfer withdrawal tx-sender contract-address)) ;; transfer back to swapr, wish there was a burn instead... 338 | (ok (list withdrawal-x withdrawal-y)) 339 | ) 340 | transfer-failed-err 341 | ) 342 | ) 343 | ) 344 | ) 345 | ) 346 | ) 347 | ) 348 | 349 | ;; exchange known dx of x-token for whatever dy of y-token based on current liquidity, returns (dx dy) 350 | ;; the swap will not happen if can't get at least min-dy back 351 | (define-public (swap-exact-x-for-y (token-x-trait ) (token-y-trait ) (dx uint) (min-dy uint)) 352 | ;; calculate dy 353 | ;; calculate fee on dx 354 | ;; transfer 355 | ;; update balances 356 | (let ((token-x (contract-of token-x-trait)) (token-y (contract-of token-y-trait))) 357 | (let ((pair (unwrap! (map-get? pairs-data-map { token-x: token-x, token-y: token-y }) invalid-pair-err))) 358 | (let 359 | ( 360 | (contract-address (as-contract tx-sender)) 361 | (sender tx-sender) 362 | (dy (/ (* u997 (get-balance token-y-trait) dx) (+ (* u1000 (get-balance token-x-trait)) (* u997 dx)))) ;; overall fee is 30 bp, either all for the pool, or 25 bp for pool and 5 bp for operator 363 | (fee (/ (* u5 dx) u10000)) ;; 5 bp 364 | ) 365 | (if (and 366 | ;; TODO(psq): check that the amount transfered in matches the amount requested 367 | (is-ok (contract-call? token-x-trait transfer dx sender contract-address)) 368 | (is-ok (as-contract (contract-call? token-y-trait transfer dy contract-address sender))) 369 | ) 370 | (begin 371 | (map-set pairs-data-map { token-x: token-x, token-y: token-y } 372 | { 373 | shares-total: (get shares-total pair), 374 | fee-balance-x: 375 | (if (is-some (get fee-to-address pair)) ;; only collect fee when fee-to-address is set 376 | (+ fee (get fee-balance-x pair)) 377 | (get fee-balance-x pair) 378 | ) 379 | , 380 | fee-balance-y: (get fee-balance-y pair), 381 | fee-to-address: (get fee-to-address pair), 382 | name: (get name pair), 383 | swapr-token: (get swapr-token pair), 384 | } 385 | ) 386 | (ok (list dx dy)) 387 | ) 388 | transfer-failed-err 389 | ) 390 | ) 391 | ) 392 | ) 393 | ) 394 | 395 | ;; exchange known dy for whatever dx based on liquidity, returns (dx dy) 396 | ;; the swap will not happen if can't get at least min-dx back 397 | (define-public (swap-exact-y-for-x (token-x-trait ) (token-y-trait ) (dy uint) (min-dx uint)) 398 | ;; calculate dx 399 | ;; calculate fee on dy 400 | ;; transfer 401 | ;; update balances 402 | (let ((token-x (contract-of token-x-trait)) (token-y (contract-of token-y-trait))) 403 | (let ((pair (unwrap! (map-get? pairs-data-map { token-x: token-x, token-y: token-y }) invalid-pair-err))) 404 | (let 405 | ( 406 | (contract-address (as-contract tx-sender)) 407 | (sender tx-sender) 408 | (dx (/ (* u997 (get-balance token-x-trait) dy) (+ (* u1000 (get-balance token-y-trait)) (* u997 dy)))) ;; overall fee is 30 bp, either all for the pool, or 25 bp for pool and 5 bp for operator 409 | (fee (/ (* u5 dy) u10000)) ;; 5 bp 410 | ) 411 | (if (and 412 | ;; TODO(psq): check that the amount transfered in matches the amount requested 413 | (is-ok (as-contract (contract-call? token-x-trait transfer dx contract-address sender))) 414 | (is-ok (contract-call? token-y-trait transfer dy sender contract-address)) 415 | ) 416 | (begin 417 | (map-set pairs-data-map { token-x: token-x, token-y: token-y } 418 | { 419 | shares-total: (get shares-total pair), 420 | ;; (balance-x (- (balance token-x-trait) dx)) ;; remove dx 421 | ;; (balance-y 422 | ;; (if (is-some (get fee-to-address pair)) ;; only collect fee when fee-to-address is set 423 | ;; (- (+ (balance token-y-trait) dy) fee) ;; add dy - fee 424 | ;; (+ (balance token-y-trait) dy) ;; add dy 425 | ;; ) 426 | ;; ) 427 | fee-balance-x: (get fee-balance-x pair), 428 | fee-balance-y: 429 | (if (is-some (get fee-to-address pair)) ;; only collect fee when fee-to-address is set 430 | (+ fee (get fee-balance-y pair)) 431 | (get fee-balance-y pair) 432 | ) 433 | , 434 | fee-to-address: (get fee-to-address pair), 435 | name: (get name pair), 436 | swapr-token: (get swapr-token pair), 437 | } 438 | ) 439 | (ok (list dx dy)) 440 | ) 441 | transfer-failed-err 442 | ) 443 | ) 444 | ) 445 | ) 446 | ) 447 | 448 | ;; ;; activate the contract fee for swaps by setting the collection address, restricted to contract owner 449 | (define-public (set-fee-to-address (token-x principal) (token-y principal) (address principal)) 450 | (let ((pair (unwrap! (map-get? pairs-data-map { token-x: token-x, token-y: token-y }) invalid-pair-err))) 451 | (if (is-eq tx-sender contract-owner) 452 | (begin 453 | (map-set pairs-data-map { token-x: token-x, token-y: token-y } 454 | { 455 | shares-total: (get shares-total pair), 456 | ;; (balance-x (balance token-x-trait)) 457 | ;; (balance-y (balance token-y-trait)) 458 | fee-balance-x: (get fee-balance-y pair), 459 | fee-balance-y: (get fee-balance-y pair), 460 | fee-to-address: (some address), 461 | name: (get name pair), 462 | swapr-token: (get swapr-token pair), 463 | } 464 | ) 465 | (ok true) 466 | ) 467 | not-owner-err 468 | ) 469 | ) 470 | ) 471 | 472 | ;; ;; clear the contract fee addres 473 | ;; ;; TODO(psq): if there are any collected fees, prevent this from happening, as the fees can no longer be collect with `collect-fees` 474 | (define-public (reset-fee-to-address (token-x principal) (token-y principal)) 475 | (let ((pair (unwrap! (map-get? pairs-data-map { token-x: token-x, token-y: token-y }) invalid-pair-err))) 476 | (if (is-eq tx-sender contract-owner) 477 | (begin 478 | (map-set pairs-data-map { token-x: token-x, token-y: token-y } 479 | { 480 | shares-total: (get shares-total pair), 481 | ;; (balance-x (balance token-x-trait)) 482 | ;; (balance-y (balance token-y-trait)) 483 | fee-balance-x: (get fee-balance-y pair), 484 | fee-balance-y: (get fee-balance-y pair), 485 | fee-to-address: none, 486 | name: (get name pair), 487 | swapr-token: (get swapr-token pair), 488 | } 489 | ) 490 | (ok true) 491 | ) 492 | not-owner-err 493 | ) 494 | ) 495 | ) 496 | 497 | ;; ;; get the current address used to collect a fee 498 | (define-read-only (get-fee-to-address (token-x principal) (token-y principal)) 499 | (let ((pair (unwrap! (map-get? pairs-data-map { token-x: token-x, token-y: token-y }) invalid-pair-err))) 500 | (ok (get fee-to-address pair)) 501 | ) 502 | ) 503 | 504 | ;; ;; get the amount of fees charged on x-token and y-token exchanges that have not been collected yet 505 | (define-read-only (get-fees (token-x principal) (token-y principal)) 506 | (let ((pair (unwrap! (map-get? pairs-data-map { token-x: token-x, token-y: token-y }) invalid-pair-err))) 507 | (ok (list (get fee-balance-x pair) (get fee-balance-y pair))) 508 | ) 509 | ) 510 | 511 | ;; ;; send the collected fees the fee-to-address 512 | (define-public (collect-fees (token-x-trait ) (token-y-trait )) 513 | (let ((token-x (contract-of token-x-trait)) (token-y (contract-of token-y-trait)) (contract-address (as-contract tx-sender))) 514 | (let ((pair (unwrap! (map-get? pairs-data-map { token-x: token-x, token-y: token-y }) invalid-pair-err))) 515 | (let ((address (unwrap! (get fee-to-address pair) no-fee-to-address-err)) (fee-x (get fee-balance-x pair)) (fee-y (get fee-balance-y pair))) 516 | (if 517 | (and 518 | (or (is-eq fee-x u0) (is-ok (as-contract (contract-call? token-x-trait transfer fee-x contract-address address)))) 519 | (or (is-eq fee-y u0) (is-ok (as-contract (contract-call? token-y-trait transfer fee-y contract-address address)))) 520 | ) 521 | (begin 522 | (map-set pairs-data-map { token-x: token-x, token-y: token-y } 523 | { 524 | shares-total: (get shares-total pair), 525 | ;; (balance-x (balance token-x-trait)) 526 | ;; (balance-y (balance token-y-trait)) 527 | fee-balance-x: u0, 528 | fee-balance-y: u0, 529 | fee-to-address: (get fee-to-address pair), 530 | name: (get name pair), 531 | swapr-token: (get swapr-token pair), 532 | } 533 | ) 534 | (ok (list fee-x fee-y)) 535 | ) 536 | transfer-failed-err 537 | ) 538 | ) 539 | ) 540 | ) 541 | ) 542 | -------------------------------------------------------------------------------- /contracts/wrapr-token.clar: -------------------------------------------------------------------------------- 1 | ;; this is the original wrapr contract, with the additional functions now required by SRC20 2 | 3 | (impl-trait 'ST3J2GVMMM2R07ZFBJDWTYEYAR8FZH5WKDTFJ9AHA.src20-trait.src20-trait) 4 | 5 | (define-data-var supply uint u0) 6 | 7 | (define-fungible-token wrapped-token) 8 | 9 | ;; get the token balance of owner 10 | (define-read-only (balance-of (owner principal)) 11 | (begin 12 | (ok (ft-get-balance wrapped-token owner)) 13 | ) 14 | ) 15 | 16 | ;; returns the total number of tokens 17 | (define-public (total-supply) 18 | (ok (var-get supply)) 19 | ) 20 | 21 | ;; returns the token name 22 | (define-public (name) 23 | (ok "wrapr") 24 | ) 25 | 26 | ;; transfer amount STX and return wrapped fungible token 27 | ;; mints new token 28 | (define-public (wrap (amount uint)) 29 | (let ((contract-address (as-contract tx-sender))) 30 | (if 31 | (and 32 | (is-ok (stx-transfer? amount tx-sender contract-address)) 33 | (is-ok (ft-mint? wrapped-token amount tx-sender)) 34 | ) 35 | (begin 36 | (var-set supply (+ (var-get supply) amount)) 37 | (ok (list amount (var-get supply))) 38 | ) 39 | (begin 40 | (err false) 41 | ) 42 | ) 43 | ) 44 | ) 45 | 46 | ;; unwraps wrapped STX token 47 | ;; burns unwrapped token (well, can't burn yet, so will forever increase, good thing there is no limit) 48 | (define-public (unwrap (amount uint)) 49 | (let ((caller tx-sender) (contract-address (as-contract tx-sender))) 50 | (if 51 | (and 52 | (<= amount (var-get supply)) 53 | ;; this is where burn would be more appropriate, as trying to reuse tokens or mint 54 | ;; would make the code more complex for little benefit 55 | (is-ok (ft-transfer? wrapped-token amount caller contract-address)) 56 | (is-ok (as-contract (stx-transfer? amount contract-address caller))) 57 | ) 58 | (begin 59 | (var-set supply (- (var-get supply) amount)) 60 | (ok (list amount (var-get supply))) 61 | ) 62 | (err false) 63 | ) 64 | ) 65 | ) 66 | 67 | ;; Transfers tokens to a specified principal. 68 | ;; just a wrapper to satisfy the `` 69 | (define-public (transfer (recipient principal) (amount uint)) 70 | (begin 71 | (ft-get-balance wrapped-token tx-sender) 72 | (ft-transfer? wrapped-token amount tx-sender recipient) 73 | ) 74 | ) 75 | 76 | -------------------------------------------------------------------------------- /deploy-contracts.ts: -------------------------------------------------------------------------------- 1 | const BigNum = require("bn.js") 2 | import * as fs from "fs" 3 | const fetch = require("node-fetch") 4 | import { 5 | broadcastTransaction, 6 | makeContractDeploy, 7 | TxBroadcastResultOk, 8 | TxBroadcastResultRejected, 9 | } from "@stacks/transactions" 10 | import { StacksTestnet, StacksMainnet } from '@stacks/network' 11 | 12 | 13 | const keys = JSON.parse( 14 | fs.readFileSync("./keychain.json").toString() 15 | ) 16 | 17 | console.log("keys", keys) 18 | const mode = process.argv[2] || 'mocknet' 19 | 20 | console.log("deploying swapr with", keys.swapr.stacksAddress, "on", mode) 21 | 22 | const STACKS_API_URL = mode === 'mocknet' ? 'http://localhost:3999' : mode === 'nanorails' ? 'https://stacks-api.nanorails.com' : 'https://stacks-node-api.mainnet.stacks.co' 23 | const network = mode === 'mainnet' ? new StacksMainnet() : new StacksTestnet() 24 | network.coreApiUrl = STACKS_API_URL 25 | console.log("using", STACKS_API_URL) 26 | 27 | async function deployContract(contract_name: string, fee: number) { 28 | console.log(`deploying ${contract_name}`) 29 | const codeBody = fs.readFileSync(`./contracts/${contract_name}.clar`).toString() 30 | 31 | const transaction = await makeContractDeploy({ 32 | contractName: contract_name, 33 | codeBody, 34 | senderKey: keys.swapr.privateKey, 35 | network, 36 | }) 37 | 38 | const result = await broadcastTransaction(transaction, network) 39 | if ((result as TxBroadcastResultRejected).error) { 40 | if ( 41 | (result as TxBroadcastResultRejected).reason === "ContractAlreadyExists" 42 | ) { 43 | console.log(`${contract_name} already deployed`) 44 | return "" as TxBroadcastResultOk 45 | } else { 46 | throw new Error( 47 | `failed to deploy ${contract_name}: ${JSON.stringify(result)}` 48 | ) 49 | } 50 | } 51 | const processed = await processing(result as TxBroadcastResultOk) 52 | if (!processed) { 53 | throw new Error(`failed to deploy ${contract_name}: transaction not found`) 54 | } 55 | return result as TxBroadcastResultOk 56 | } 57 | 58 | function timeout(ms: number) { 59 | return new Promise((resolve) => setTimeout(resolve, ms)) 60 | } 61 | 62 | async function processing(tx: String, count: number = 0): Promise { 63 | console.log("processing", tx, count) 64 | var result = await fetch( 65 | `${STACKS_API_URL}/extended/v1/tx/${tx}` 66 | ) 67 | var value = await result.json() 68 | if (value.tx_status === "success") { 69 | console.log(`transaction ${tx} processed`) 70 | // console.log(value) 71 | return true 72 | } 73 | // if (value.tx_status === "pending") { 74 | // console.log("pending" /*, value*/) 75 | // } 76 | if (count > 10) { 77 | console.log("failed after 2 attempts", value) 78 | return false 79 | } 80 | 81 | await timeout(15000) 82 | return processing(tx, count + 1) 83 | } 84 | 85 | (async () => { 86 | await deployContract('src20-trait', 3000) 87 | await deployContract('plaid-token', 3000) 88 | await deployContract('swapr-trait', 3000) 89 | await deployContract('swapr', 3000) 90 | await deployContract('stx-token', 3000) 91 | await deployContract('oracle', 3000) 92 | await deployContract('flexr-token', 3000) 93 | await deployContract('flexr-stx-token', 3000) 94 | await deployContract('plaid-stx-token', 3000) 95 | await deployContract('geyser', 3000) 96 | 97 | 98 | // create flexr-stx pair 99 | // create plaid-stx pair 100 | 101 | })() 102 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "flexr", 3 | "version": "0.0.1", 4 | "author": { 5 | "name": "Pascal Belloncle", 6 | "email": "psq@nanorails.com" 7 | }, 8 | "repository": "https://github.com/psq/flexr.git", 9 | "scripts": { 10 | "build": "rimraf ./lib && tsc", 11 | "deploy-mocknet": "ts-node ./deploy-contracts mocknet", 12 | "deploy-testnet": "ts-node ./deploy-contracts testnet", 13 | "test": "mocha 'test/unit/*.ts'" 14 | }, 15 | "dependencies": { 16 | "@blockstack/clarity-cli": "^0.3.5", 17 | "@stacks/network": "^1.2.2", 18 | "@stacks/transactions": "^1.0.0-beta.20", 19 | "node-fetch": "^2.6.1" 20 | }, 21 | "devDependencies": { 22 | "@types/chai": "^4.1.7", 23 | "@types/mocha": "^5.2.7", 24 | "@types/node": "^10", 25 | "chai": "^4.2.0", 26 | "chai-string": "^1.5.0", 27 | "mocha": "^6.1.4", 28 | "rimraf": "^3.0.2", 29 | "ts-node": "^8.2.0", 30 | "typescript": "^3.5.2" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /scenario.md: -------------------------------------------------------------------------------- 1 | # Test scenario 2 | - initialize all [contracts](./contracts). There are 2 traits, and 6 contracts interacting with each order. The order in which you deploy them is important! 3 | - init flexr treasury with [flexr](./contracts/flexr-token.clar) tokens (see line before last) and enough [stx](./balances.json) for funding swapr 4 | - setup flexr-wrapr pair in swapr and fund from flexr treasury (as STX is not a token, `wrapr` is used to wrap STX into an [SRC20 token](./contracts/src20-trait.clar) that can be used with `swapr`) 5 | - fund Geyser from flexr treasury (done when deploying the [flexr](./contracts/flexr-token.clar) contract, see last line) 6 | 7 | - fund user[1-3] with [stx](./balances.json) 8 | - user[1-3] wrap stx using `wrapr` 9 | - user[1-3] buy flexr tokens 10 | - user1 provide liquidity to the flexr-stx pair and gets back liquidity token 11 | - user1 stakes his flexr-stx pair in the geyser 12 | 13 | - go through a few price updates and rebase (how to advance block faster with clarity-js-sdk?) 14 | - user3 tries rebase arbitrage (TBD) 15 | 16 | - user1 withdraws reward from geyser and checks earnings 17 | - user1 withdraw liquity and checks earnings (TBD) 18 | 19 | # Scenario run output 20 | ``` 21 | initialize balances.json /var/folders/10/vfzc9gqn6cs8d2zldp6v83400000gp/T/blockstack-local-1598255557-djwmpi.db 22 | Check contracts 23 | ✓ should have a valid syntax (19272ms) 24 | Full scenario 25 | ======> wrap.treasury 26 | ======> createPair.treasury 27 | ======> wrap.alice 28 | ======> swapExactYforX.alice 29 | ======> addToPosition.alice 30 | ======> stake.alice 31 | ======> wrap.bob 32 | ======> wrap.zoe 33 | ======> wrap.zoe 34 | ======> swapExactYforX.bob - round 0 35 | ======> swapExactYforX.bob 36 | ======> swapExactXforY.zoe 37 | ======> updatePrice.zoe 38 | ======> rebase.zoe - 1100000 39 | ======> swapExactYforX.bob - round 1 40 | ======> swapExactYforX.bob 41 | ======> swapExactXforY.zoe 42 | ======> updatePrice.zoe 43 | ======> rebase.zoe - 1150000 44 | ======> swapExactYforX.bob - round 2 45 | ======> swapExactYforX.bob 46 | ======> swapExactXforY.zoe 47 | ======> updatePrice.zoe 48 | ======> rebase.zoe - 1050000 49 | ======> swapExactYforX.bob - round 3 50 | ======> swapExactYforX.bob 51 | ======> swapExactXforY.zoe 52 | ======> updatePrice.zoe 53 | ======> rebase.zoe - 950000 54 | ======> swapExactYforX.bob - round 4 55 | ======> swapExactYforX.bob 56 | ======> swapExactXforY.zoe 57 | ======> updatePrice.zoe 58 | ======> rebase.zoe - 900000 59 | ======> unstake.alice 60 | ✓ check balances after running scenario (206ms) 61 | 62 | 63 | 2 passing (24s) 64 | 65 | ✨ Done in 24.57s. 66 | ``` 67 | 68 | Alice got 960_000 micro flexr after just a few blocks 😂 69 | -------------------------------------------------------------------------------- /src/clients/flexr-client.ts: -------------------------------------------------------------------------------- 1 | import { Client, Provider, Receipt, Result } from '@blockstack/clarity' 2 | import { 3 | TransferError, 4 | } from '../errors' 5 | 6 | import { 7 | parse, 8 | unwrapXYList, 9 | unwrapSome, 10 | unwrapOK, 11 | } from '../utils' 12 | 13 | export class FlexrClient extends Client { 14 | token_name: string 15 | 16 | constructor(principal: string, provider: Provider) { 17 | super( 18 | `${principal}.flexr-token`, 19 | 'flexr-token', 20 | provider 21 | ) 22 | } 23 | 24 | async transfer(recipient: string, amount: number, params: { sender: string }): Promise { 25 | const tx = this.createTransaction({ 26 | method: { name: "transfer", args: [`'${recipient}`, `u${amount}`] } 27 | }) 28 | await tx.sign(params.sender) 29 | const receipt = await this.submitTransaction(tx) 30 | if (receipt.success) { 31 | // console.log("debugOutput", receipt.debugOutput) 32 | const result = Result.unwrap(receipt) 33 | return result.startsWith('Transaction executed and committed. Returned: true') 34 | } 35 | throw TransferError 36 | } 37 | 38 | async rebase(params: { sender: string }): Promise { 39 | const tx = this.createTransaction({ 40 | method: { name: "rebase", args: [] } 41 | }) 42 | await tx.sign(params.sender) 43 | const receipt = await this.submitTransaction(tx) 44 | if (receipt.success) { 45 | // console.log("debugOutput.rebase", receipt.debugOutput) 46 | const result = Result.unwrap(receipt) 47 | return result.startsWith('Transaction executed and committed. Returned: true') 48 | } 49 | console.log("rebase", receipt) 50 | throw TransferError 51 | } 52 | 53 | async balanceOf(owner: string): Promise { 54 | const query = this.createQuery({ 55 | method: { 56 | name: 'balance-of', 57 | args: [`'${owner}`], 58 | }, 59 | }) 60 | const receipt = await this.submitQuery(query) 61 | return Result.unwrapUInt(receipt) 62 | } 63 | 64 | async totalSupply(): Promise { 65 | const query = this.createQuery({ 66 | method: { 67 | name: 'total-supply', 68 | args: [``], 69 | }, 70 | }) 71 | const receipt = await this.submitQuery(query) 72 | return Result.unwrapUInt(receipt) 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /src/clients/flexr-stx-token-client.ts: -------------------------------------------------------------------------------- 1 | import { Client, Provider, Receipt, Result } from '@blockstack/clarity' 2 | import { 3 | TransferError, 4 | } from '../errors' 5 | 6 | import { 7 | parse, 8 | unwrapXYList, 9 | unwrapSome, 10 | unwrapOK, 11 | } from '../utils' 12 | 13 | export class FlexrStxTokenClient extends Client { 14 | token_name: string 15 | 16 | constructor(name: string, principal: string, provider: Provider) { 17 | super( 18 | `${principal}.flexr-stx-token`, 19 | 'flexr-stx-token', 20 | provider 21 | ) 22 | this.token_name = name 23 | } 24 | 25 | async transfer(recipient: string, amount: number, params: { sender: string }): Promise { 26 | const tx = this.createTransaction({ 27 | method: { name: "transfer", args: [`'${recipient}`, `u${amount}`] } 28 | }) 29 | await tx.sign(params.sender) 30 | const receipt = await this.submitTransaction(tx) 31 | if (receipt.success) { 32 | // console.log("debugOutput", receipt.debugOutput) 33 | const result = Result.unwrap(receipt) 34 | return result.startsWith('Transaction executed and committed. Returned: true') 35 | } 36 | throw TransferError 37 | } 38 | 39 | async balanceOf(owner: string): Promise { 40 | const query = this.createQuery({ 41 | method: { 42 | name: 'balance-of', 43 | args: [`'${owner}`], 44 | }, 45 | }) 46 | const receipt = await this.submitQuery(query) 47 | console.log("balanceOf.receipt", receipt) 48 | return Result.unwrapUInt(receipt) 49 | } 50 | 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/clients/geyser-client.ts: -------------------------------------------------------------------------------- 1 | import { Client, Provider, Receipt, Result } from '@blockstack/clarity' 2 | import { 3 | TransferError, 4 | } from '../errors' 5 | 6 | import { 7 | parse, 8 | unwrapXYList, 9 | unwrapSome, 10 | unwrapOK, 11 | } from '../utils' 12 | 13 | export class GeyserClient extends Client { 14 | token_name: string 15 | 16 | constructor(principal: string, provider: Provider) { 17 | super( 18 | `${principal}.geyser`, 19 | 'geyser', 20 | provider 21 | ) 22 | } 23 | 24 | async stake(amount: number, params: { sender: string }): Promise { 25 | const tx = this.createTransaction({ 26 | method: { name: "stake", args: [`u${amount}`] } 27 | }) 28 | await tx.sign(params.sender) 29 | const receipt = await this.submitTransaction(tx) 30 | if (receipt.success) { 31 | console.log("debugOutput", receipt.debugOutput) 32 | const result = Result.unwrap(receipt) 33 | return result.startsWith('Transaction executed and committed. Returned: true') 34 | } 35 | console.log("stake", receipt) 36 | throw TransferError 37 | } 38 | 39 | async unstake(params: { sender: string }): Promise { 40 | const tx = this.createTransaction({ 41 | method: { name: "unstake", args: [] } 42 | }) 43 | await tx.sign(params.sender) 44 | const receipt = await this.submitTransaction(tx) 45 | if (receipt.success) { 46 | console.log("debugOutput", receipt.debugOutput) 47 | const result = Result.unwrap(receipt) 48 | return result.startsWith('Transaction executed and committed. Returned: true') 49 | } 50 | console.log("unstake", receipt) 51 | throw TransferError 52 | } 53 | 54 | async totalSupply(owner: string): Promise { 55 | const query = this.createQuery({ 56 | method: { 57 | name: 'total-supply', 58 | args: [], 59 | }, 60 | }) 61 | const receipt = await this.submitQuery(query) 62 | return Result.unwrapUInt(receipt) 63 | } 64 | 65 | 66 | } 67 | -------------------------------------------------------------------------------- /src/clients/oracle-client.ts: -------------------------------------------------------------------------------- 1 | import { Client, Provider, Receipt, Result } from '@blockstack/clarity' 2 | import { 3 | TransferError, 4 | } from '../errors' 5 | 6 | import { 7 | parse, 8 | unwrapXYList, 9 | unwrapSome, 10 | unwrapOK, 11 | } from '../utils' 12 | 13 | export class OracleClient extends Client { 14 | token_name: string 15 | 16 | constructor(principal: string, provider: Provider) { 17 | super( 18 | `${principal}.oracle`, 19 | 'oracle', 20 | provider 21 | ) 22 | } 23 | 24 | async updatePrice(price: number, params: { sender: string }): Promise { 25 | const tx = this.createTransaction({ 26 | method: { name: "update-price", args: [`u${price}`] } 27 | }) 28 | await tx.sign(params.sender) 29 | const receipt = await this.submitTransaction(tx) 30 | if (receipt.success) { 31 | // console.log("updatePrice.debugOutput", receipt.debugOutput) 32 | const result = Result.unwrap(receipt) 33 | return result.startsWith('Transaction executed and committed. Returned: true') 34 | } 35 | console.log("updatePrice", receipt) 36 | throw TransferError 37 | } 38 | 39 | // async balanceOf(owner: string): Promise { 40 | // const query = this.createQuery({ 41 | // method: { 42 | // name: 'balance-of', 43 | // args: [`'${owner}`], 44 | // }, 45 | // }) 46 | // const receipt = await this.submitQuery(query) 47 | // return Result.unwrapUInt(receipt) 48 | // } 49 | 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/clients/plaid-client.ts: -------------------------------------------------------------------------------- 1 | import { Client, Provider, Receipt, Result } from '@blockstack/clarity' 2 | import { 3 | TransferError, 4 | } from '../errors' 5 | 6 | export class PlaidClient extends Client { 7 | constructor(principal: string, provider: Provider) { 8 | super( 9 | `${principal}.plaid-token`, 10 | 'plaid-token', 11 | provider 12 | ) 13 | } 14 | 15 | async transfer(recipient: string, amount: number, params: { sender: string }): Promise { 16 | const tx = this.createTransaction({ 17 | method: { name: "transfer", args: [`'${recipient}`, `u${amount}`] } 18 | }) 19 | await tx.sign(params.sender) 20 | const receipt = await this.submitTransaction(tx) 21 | // console.log(receipt) 22 | if (receipt.success) { 23 | // console.log("debugOutput", receipt.debugOutput) 24 | const result = Result.unwrap(receipt) 25 | return result.startsWith('Transaction executed and committed. Returned: true') 26 | } 27 | throw new TransferError() 28 | } 29 | 30 | async balanceOf(owner: string): Promise { 31 | const query = this.createQuery({ 32 | method: { 33 | name: 'balance-of', 34 | args: [`'${owner}`], 35 | }, 36 | }) 37 | const receipt = await this.submitQuery(query) 38 | console.log("receipt", receipt) 39 | return Result.unwrapUInt(receipt) 40 | } 41 | 42 | async totalSupply(): Promise { 43 | const query = this.createQuery({ 44 | method: { 45 | name: 'total-supply', 46 | args: [], 47 | }, 48 | }) 49 | const receipt = await this.submitQuery(query) 50 | console.log("receipt", receipt) 51 | return Result.unwrapUInt(receipt) 52 | } 53 | 54 | async name(): Promise { 55 | const query = this.createQuery({ 56 | method: { 57 | name: 'name', 58 | args: [], 59 | }, 60 | }) 61 | const receipt = await this.submitQuery(query) 62 | console.log("receipt", receipt) 63 | return Result.unwrap(receipt) 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /src/clients/plaid-stx-token-client.ts: -------------------------------------------------------------------------------- 1 | import { Client, Provider, Receipt, Result } from '@blockstack/clarity' 2 | import { 3 | TransferError, 4 | } from '../errors' 5 | 6 | import { 7 | parse, 8 | unwrapXYList, 9 | unwrapSome, 10 | unwrapOK, 11 | } from '../utils' 12 | 13 | export class PlaidStxTokenClient extends Client { 14 | token_name: string 15 | 16 | constructor(name: string, principal: string, provider: Provider) { 17 | super( 18 | `${principal}.plaid-stx-token`, 19 | 'plaid-stx-token', 20 | provider 21 | ) 22 | this.token_name = name 23 | } 24 | 25 | async transfer(recipient: string, amount: number, params: { sender: string }): Promise { 26 | const tx = this.createTransaction({ 27 | method: { name: "transfer", args: [`'${recipient}`, `u${amount}`] } 28 | }) 29 | await tx.sign(params.sender) 30 | const receipt = await this.submitTransaction(tx) 31 | if (receipt.success) { 32 | // console.log("debugOutput", receipt.debugOutput) 33 | const result = Result.unwrap(receipt) 34 | return result.startsWith('Transaction executed and committed. Returned: true') 35 | } 36 | throw TransferError 37 | } 38 | 39 | async balanceOf(owner: string): Promise { 40 | const query = this.createQuery({ 41 | method: { 42 | name: 'balance-of', 43 | args: [`'${owner}`], 44 | }, 45 | }) 46 | const receipt = await this.submitQuery(query) 47 | console.log("balanceOf.receipt", receipt) 48 | return Result.unwrapUInt(receipt) 49 | } 50 | 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/clients/stx-client.ts: -------------------------------------------------------------------------------- 1 | import { Client, Provider, Receipt, Result } from '@blockstack/clarity' 2 | import { 3 | TransferError, 4 | } from '../errors' 5 | 6 | export class StxClient extends Client { 7 | constructor(principal: string, provider: Provider) { 8 | super( 9 | `${principal}.stx-token`, 10 | 'stx-token', 11 | provider 12 | ) 13 | } 14 | 15 | async transfer(recipient: string, amount: number, params: { sender: string }): Promise { 16 | const tx = this.createTransaction({ 17 | method: { name: "transfer", args: [`'${recipient}`, `u${amount}`] } 18 | }) 19 | await tx.sign(params.sender) 20 | const receipt = await this.submitTransaction(tx) 21 | // console.log(receipt) 22 | if (receipt.success) { 23 | // console.log("debugOutput", receipt.debugOutput) 24 | const result = Result.unwrap(receipt) 25 | return result.startsWith('Transaction executed and committed. Returned: true') 26 | } 27 | throw new TransferError() 28 | } 29 | 30 | async balanceOf(owner: string): Promise { 31 | const query = this.createQuery({ 32 | method: { 33 | name: 'balance-of', 34 | args: [`'${owner}`], 35 | }, 36 | }) 37 | const receipt = await this.submitQuery(query) 38 | console.log("receipt", receipt) 39 | return Result.unwrapUInt(receipt) 40 | } 41 | 42 | async totalSupply(): Promise { 43 | const query = this.createQuery({ 44 | method: { 45 | name: 'total-supply', 46 | args: [], 47 | }, 48 | }) 49 | const receipt = await this.submitQuery(query) 50 | console.log("receipt", receipt) 51 | return Result.unwrapUInt(receipt) 52 | } 53 | 54 | async name(): Promise { 55 | const query = this.createQuery({ 56 | method: { 57 | name: 'name', 58 | args: [], 59 | }, 60 | }) 61 | const receipt = await this.submitQuery(query) 62 | console.log("receipt", receipt) 63 | return Result.unwrap(receipt) 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /src/clients/swapr-client.ts: -------------------------------------------------------------------------------- 1 | import { Client, Provider, Receipt, Result } from '@blockstack/clarity' 2 | import { 3 | ClarityParseError, 4 | NoLiquidityError, 5 | NotOwnerError, 6 | NotOKErr, 7 | NotSomeErr, 8 | } from '../errors' 9 | 10 | import { 11 | parse, 12 | unwrapXYList, 13 | unwrapSome, 14 | unwrapOK, 15 | } from '../utils' 16 | 17 | export class SwaprClient extends Client { 18 | constructor(principal: string, provider: Provider) { 19 | super( 20 | `${principal}.swapr`, 21 | 'swapr', 22 | provider 23 | ) 24 | } 25 | 26 | async createPair(token_x_token: string, token_y_token: string, swapr_token: string, name: string, x: number, y: number, params: { sender: string }): Promise { 27 | // console.log("createPair.args", [`${token_x_token}`, `${token_y_token}`, `${swapr_token}`, `"${name}"`, `u${x}`, `u${y}`]) 28 | const tx = this.createTransaction({ 29 | method: { name: "create-pair", args: [`'${token_x_token}`, `'${token_y_token}`, `'${swapr_token}`, `"${name}"`, `u${x}`, `u${y}`] } 30 | }) 31 | await tx.sign(params.sender) 32 | const receipt = await this.submitTransaction(tx) 33 | if (receipt.success) { 34 | // console.log("createPair", receipt.debugOutput) 35 | const result = Result.unwrap(receipt) 36 | // console.log("createPair.result", result) 37 | return result.startsWith('Transaction executed and committed. Returned: true') 38 | } 39 | console.log("createPair failure", receipt) 40 | throw NotOKErr 41 | } 42 | 43 | async addToPosition(token_x_token: string, token_y_token: string, swapr_token: string, x: number, y: number, params: { sender: string }): Promise { 44 | const tx = this.createTransaction({ 45 | method: { name: "add-to-position", args: [`'${token_x_token}`, `'${token_y_token}`, `'${swapr_token}`, `u${x}`, `u${y}`] } 46 | }) 47 | await tx.sign(params.sender) 48 | const receipt = await this.submitTransaction(tx) 49 | if (receipt.success) { 50 | console.log(receipt.debugOutput) 51 | const result = Result.unwrap(receipt) 52 | return result.startsWith('Transaction executed and committed. Returned: true') 53 | } 54 | console.log("addToPosition failure", receipt) 55 | throw NotOKErr 56 | 57 | } 58 | 59 | async reducePosition(token_x_token: string, token_y_token: string, swapr_token: string, percent: number, params: { sender: string }): Promise { 60 | const tx = this.createTransaction({ 61 | method: { name: "reduce-position", args: [`'${token_x_token}`, `'${token_y_token}`, `'${swapr_token}`, `u${percent}`] } 62 | }) 63 | await tx.sign(params.sender) 64 | const receipt = await this.submitTransaction(tx) 65 | // console.log("debugOutput", receipt.debugOutput) 66 | const result = Result.unwrap(receipt) 67 | 68 | if (result.startsWith('Transaction executed and committed. Returned: ')) { 69 | const start_of_list = result.substring('Transaction executed and committed. Returned: '.length) // keep a word so unwrapXYList will behave like it was with 'ok' 70 | const parsed = parse(start_of_list.substring(0, start_of_list.indexOf(')') + 1)) 71 | return unwrapXYList(parsed) 72 | } 73 | throw new NotOKErr() 74 | } 75 | 76 | async swapExactXforY(token_x_token: string, token_y_token: string, dx: number, params: { sender: string }): Promise { 77 | const tx = this.createTransaction({ 78 | method: { name: "swap-exact-x-for-y", args: [`'${token_x_token}`, `'${token_y_token}`, `u${dx}`] } 79 | }) 80 | await tx.sign(params.sender) 81 | const receipt = await this.submitTransaction(tx) 82 | // console.log("debugOutput", receipt.debugOutput) 83 | const result = Result.unwrap(receipt) 84 | 85 | if (result.startsWith('Transaction executed and committed. Returned: ')) { 86 | const start_of_list = result.substring('Transaction executed and committed. Returned: '.length) // keep a word so unwrapXYList will behave like it was with 'ok' 87 | const parsed = parse(start_of_list.substring(0, start_of_list.indexOf(')') + 1)) 88 | return unwrapXYList(parsed) 89 | } 90 | throw new NotOKErr() 91 | } 92 | 93 | async swapXforExactY(token_x_token: string, token_y_token: string, dy: number, params: { sender: string }): Promise { 94 | const tx = this.createTransaction({ 95 | method: { name: "swap-x-for-exact-y", args: [`'${token_x_token}`, `'${token_y_token}`, `u${dy}`] } 96 | }) 97 | await tx.sign(params.sender) 98 | const receipt = await this.submitTransaction(tx) 99 | // console.log("debugOutput", receipt.debugOutput) 100 | const result = Result.unwrap(receipt) 101 | 102 | if (result.startsWith('Transaction executed and committed. Returned: ')) { 103 | const start_of_list = result.substring('Transaction executed and committed. Returned: '.length) // keep a word so unwrapXYList will behave like it was with 'ok' 104 | const parsed = parse(start_of_list.substring(0, start_of_list.indexOf(')') + 1)) 105 | return unwrapXYList(parsed) 106 | } 107 | throw new NotOKErr() 108 | } 109 | 110 | async swapExactYforX(token_x_token: string, token_y_token: string, dy: number, params: { sender: string }): Promise { 111 | const tx = this.createTransaction({ 112 | method: { name: "swap-exact-y-for-x", args: [`'${token_x_token}`, `'${token_y_token}`, `u${dy}`] } 113 | }) 114 | await tx.sign(params.sender) 115 | const receipt = await this.submitTransaction(tx) 116 | // console.log("debugOutput", receipt.debugOutput) 117 | const result = Result.unwrap(receipt) 118 | 119 | if (result.startsWith('Transaction executed and committed. Returned: ')) { 120 | const start_of_list = result.substring('Transaction executed and committed. Returned: '.length) // keep a word so unwrapXYList will behave like it was with 'ok' 121 | const parsed = parse(start_of_list.substring(0, start_of_list.indexOf(')') + 1)) 122 | return unwrapXYList(parsed) 123 | } 124 | throw new NotOKErr() 125 | } 126 | 127 | async swapYforExactX(token_x_token: string, token_y_token: string, dx: number, params: { sender: string }): Promise { 128 | const tx = this.createTransaction({ 129 | method: { name: "swap-y-for-exact-x", args: [`'${token_x_token}`, `'${token_y_token}`, `u${dx}`] } 130 | }) 131 | await tx.sign(params.sender) 132 | const receipt = await this.submitTransaction(tx) 133 | // console.log("debugOutput", receipt.debugOutput) 134 | const result = Result.unwrap(receipt) 135 | 136 | if (result.startsWith('Transaction executed and committed. Returned: ')) { 137 | const start_of_list = result.substring('Transaction executed and committed. Returned: '.length) // keep a word so unwrapXYList will behave like it was with 'ok' 138 | const parsed = parse(start_of_list.substring(0, start_of_list.indexOf(')') + 1)) 139 | return unwrapXYList(parsed) 140 | } 141 | throw new NotOKErr() 142 | } 143 | 144 | async positionOf(token_x_token: string, token_y_token: string, owner: string): Promise { 145 | const query = this.createQuery({ 146 | method: { 147 | name: 'get-position-of', 148 | args: [`'${token_x_token}`, `'${token_y_token}`, `'${owner}`], 149 | }, 150 | }) 151 | const receipt = await this.submitQuery(query) 152 | return Result.unwrapUInt(receipt) 153 | } 154 | 155 | async balances(): Promise { 156 | const query = this.createQuery({ 157 | method: { 158 | name: 'get-balances', 159 | args: [`'${token_x_token}`, `'${token_y_token}`], 160 | }, 161 | }) 162 | const receipt = await this.submitQuery(query) 163 | return unwrapXYList(unwrapOK(parse(Result.unwrap(receipt)))) 164 | } 165 | 166 | async positions(): Promise { 167 | const query = this.createQuery({ 168 | method: { 169 | name: 'get-positions', 170 | args: [], 171 | }, 172 | }) 173 | const receipt = await this.submitQuery(query) 174 | return Result.unwrapUInt(receipt) 175 | } 176 | 177 | async balancesOf(owner: string): Promise { 178 | const query = this.createQuery({ 179 | method: { 180 | name: 'get-balances-of', 181 | args: [`'${owner}`], 182 | }, 183 | }) 184 | const receipt = await this.submitQuery(query) 185 | // console.log("balancesOf", receipt) 186 | const result = Result.unwrap(receipt) 187 | if (result.startsWith('(err')) { 188 | throw new NoLiquidityError() 189 | } else { 190 | return unwrapXYList(unwrapOK(parse(result))) 191 | } 192 | } 193 | 194 | async setFeeTo(address: string, params: { sender: string }): Promise { 195 | const tx = this.createTransaction({ 196 | method: { name: "set-fee-to-address", args: [`'${address}`] } 197 | }) 198 | await tx.sign(params.sender) 199 | const receipt = await this.submitTransaction(tx) 200 | // console.log("receipt", receipt) 201 | // console.log("debugOutput", receipt.debugOutput) 202 | if (receipt.success) { 203 | const result = Result.unwrap(receipt) 204 | // console.log("result", result) 205 | if (result.startsWith('Transaction executed and committed. Returned: ')) { 206 | const start = result.substring('Transaction executed and committed. Returned: '.length) 207 | const extracted = start.substring(0, start.indexOf('\n')) 208 | // console.log("extracted", `=${extracted}=`) 209 | if (extracted === 'true') { 210 | return true 211 | } 212 | } 213 | } 214 | throw new NotOwnerError() 215 | } 216 | 217 | async resetFeeTo(params: { sender: string }): Promise { 218 | const tx = this.createTransaction({ 219 | method: { name: "reset-fee-to-address", args: [] } 220 | }) 221 | await tx.sign(params.sender) 222 | const receipt = await this.submitTransaction(tx) 223 | // console.log("receipt", receipt) 224 | // console.log("debugOutput", receipt.debugOutput) 225 | if (receipt.success) { 226 | const result = Result.unwrap(receipt) 227 | // console.log("result", result) 228 | if (result.startsWith('Transaction executed and committed. Returned: ')) { 229 | const start = result.substring('Transaction executed and committed. Returned: '.length) 230 | const extracted = start.substring(0, start.indexOf('\n')) 231 | // console.log("extracted", `=${extracted}=`) 232 | if (extracted === 'true') { 233 | return true 234 | } 235 | } 236 | } 237 | throw new NotOwnerError() 238 | } 239 | 240 | async collectFees(params: { sender: string }): Promise { 241 | const tx = this.createTransaction({ 242 | method: { name: "collect-fees", args: [] } 243 | }) 244 | await tx.sign(params.sender) 245 | const receipt = await this.submitTransaction(tx) 246 | // console.log("receipt", receipt) 247 | console.log("debugOutput", receipt.debugOutput) 248 | if (receipt.success) { 249 | const result = Result.unwrap(receipt) 250 | // console.log("result", result) 251 | if (result.startsWith('Transaction executed and committed. Returned: ')) { 252 | const start_of_list = result.substring('Transaction executed and committed. Returned: '.length) // keep a word so unwrapXYList will behave like it was with 'ok' 253 | const parsed = parse(start_of_list.substring(0, start_of_list.indexOf(')') + 1)) 254 | return unwrapXYList(parsed) 255 | } 256 | } 257 | throw new NotOwnerError() 258 | } 259 | 260 | async getFeeTo(): Promise { 261 | const query = this.createQuery({ 262 | atChaintip: true, 263 | method: { name: "get-fee-to-address", args: [] } 264 | }) 265 | const result = await this.submitQuery(query) 266 | // console.log("getFeeTo", Result.unwrap(result)) 267 | const value = unwrapOK(parse(Result.unwrap(result))) 268 | return (value === 'none' ? null : unwrapSome(value)) 269 | } 270 | 271 | async fees(): Promise { 272 | const query = this.createQuery({ 273 | method: { 274 | name: 'get-fees', 275 | args: [], 276 | }, 277 | }) 278 | const receipt = await this.submitQuery(query) 279 | return unwrapXYList(unwrapOK(parse(Result.unwrap(receipt)))) 280 | } 281 | 282 | } 283 | -------------------------------------------------------------------------------- /src/clients/wrapr-client.ts: -------------------------------------------------------------------------------- 1 | import { Client, Provider, Receipt, Result } from '@blockstack/clarity' 2 | import { 3 | TransferError, 4 | } from '../errors' 5 | 6 | import { 7 | parse, 8 | unwrapXYList, 9 | unwrapSome, 10 | unwrapOK, 11 | } from '../utils' 12 | 13 | export class WraprClient extends Client { 14 | constructor(principal: string, provider: Provider) { 15 | super( 16 | `${principal}.wrapr-token`, 17 | 'wrapr-token', 18 | provider 19 | ) 20 | } 21 | 22 | async wrap(amount: number, params: { sender: string }): Promise { 23 | const tx = this.createTransaction({ 24 | method: { name: "wrap", args: [`u${amount}`] } 25 | }); 26 | await tx.sign(params.sender) 27 | const receipt = await this.submitTransaction(tx) 28 | if (receipt.success) { 29 | // console.log("wrap", receipt.debugOutput) 30 | const result = Result.unwrap(receipt) 31 | // console.log("result", result) 32 | return result.startsWith('Transaction executed and committed') 33 | } 34 | throw new TransferError() 35 | } 36 | 37 | async unwrap(amount: number, params: { sender: string }): Promise { 38 | const tx = this.createTransaction({ 39 | method: { name: "unwrap", args: [`u${amount}`] } 40 | }) 41 | await tx.sign(params.sender) 42 | const receipt = await this.submitTransaction(tx) 43 | if (receipt.success) { 44 | // console.log("debugOutput", receipt.debugOutput) 45 | const result = Result.unwrap(receipt) 46 | return result.startsWith('Transaction executed and committed. Returned: true') 47 | } 48 | throw new TransferError() 49 | } 50 | 51 | async transfer(recipient: string, amount: number, params: { sender: string }): Promise { 52 | const tx = this.createTransaction({ 53 | method: { name: "transfer", args: [`'${recipient}`, `u${amount}`] } 54 | }) 55 | await tx.sign(params.sender) 56 | const receipt = await this.submitTransaction(tx) 57 | // console.log(receipt) 58 | if (receipt.success) { 59 | // console.log("debugOutput", receipt.debugOutput) 60 | const result = Result.unwrap(receipt) 61 | return result.startsWith('Transaction executed and committed. Returned: true') 62 | } 63 | throw new TransferError() 64 | } 65 | 66 | async balanceOf(owner: string): Promise { 67 | const query = this.createQuery({ 68 | method: { 69 | name: 'balance-of', 70 | args: [`'${owner}`], 71 | }, 72 | }) 73 | const receipt = await this.submitQuery(query) 74 | console.log("receipt", receipt) 75 | return Result.unwrapUInt(receipt) 76 | } 77 | 78 | async totalSupply(): Promise { 79 | const query = this.createQuery({ 80 | method: { 81 | name: 'total-supply', 82 | args: [], 83 | }, 84 | }) 85 | const receipt = await this.submitQuery(query) 86 | console.log("receipt", receipt) 87 | return Result.unwrapUInt(receipt) 88 | } 89 | 90 | } 91 | -------------------------------------------------------------------------------- /src/errors.ts: -------------------------------------------------------------------------------- 1 | export class NoLiquidityError extends Error { 2 | constructor(message?: string) { 3 | super(message) 4 | Object.setPrototypeOf(this, new.target.prototype) // restore prototype chain 5 | } 6 | } 7 | 8 | export class NotOwnerError extends Error { 9 | constructor(message?: string) { 10 | super(message) 11 | Object.setPrototypeOf(this, new.target.prototype) // restore prototype chain 12 | } 13 | } 14 | 15 | export class NotOKErr extends Error { 16 | constructor(message?: string) { 17 | super(message) 18 | Object.setPrototypeOf(this, new.target.prototype) // restore prototype chain 19 | } 20 | } 21 | 22 | export class NotSomeErr extends Error { 23 | constructor(message?: string) { 24 | super(message) 25 | Object.setPrototypeOf(this, new.target.prototype) // restore prototype chain 26 | } 27 | } 28 | 29 | export class ClarityParseError extends Error { 30 | constructor(message?: string) { 31 | super(message) 32 | Object.setPrototypeOf(this, new.target.prototype) // restore prototype chain 33 | } 34 | } 35 | 36 | export class TransferError extends Error { 37 | constructor(message?: string) { 38 | super(message) 39 | Object.setPrototypeOf(this, new.target.prototype) // restore prototype chain 40 | } 41 | } 42 | 43 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ClarityParseError, 3 | NoLiquidityError, 4 | NotOwnerError, 5 | NotOKErr, 6 | NotSomeErr, 7 | } from './errors' 8 | 9 | export function parse(value: string) { 10 | let index = 0 11 | function sub() { 12 | const keywords = [] 13 | let current = index 14 | function saveKeyword() { 15 | if (index - 1 > current) { 16 | // @ts-ignore 17 | keywords.push(value.slice(current, index - 1)) 18 | } 19 | } 20 | while (index < value.length) { 21 | const c = value[index++] 22 | // console.log("c", c, index) 23 | if (c === '(') { 24 | // @ts-ignore 25 | keywords.push(sub()) 26 | current = index 27 | } else if (c === ')') { 28 | saveKeyword() 29 | return keywords 30 | } else if (c === ' ') { 31 | saveKeyword() 32 | current = index 33 | } 34 | } 35 | saveKeyword() 36 | return keywords 37 | } 38 | return sub()[0] 39 | } 40 | 41 | export function unwrapList(tree: any) { 42 | // console.log("unwrapList", tree) 43 | return tree 44 | } 45 | 46 | export function unwrapXYList(tree: any) { 47 | // console.log("unwrapXYList", tree) 48 | return { 49 | x: parseInt(tree[0].substring(1)), 50 | y: parseInt(tree[1].substring(1)), 51 | } 52 | } 53 | 54 | export function unwrapSome(tree: any): any { 55 | // console.log("unwrapSome", tree) 56 | if (tree[0] === 'some') { 57 | return tree[1] 58 | } else { 59 | throw NotSomeErr 60 | } 61 | } 62 | 63 | export function unwrapOK(tree) { 64 | // console.log("unwrapOK", tree) 65 | if (tree[0] === 'ok') { 66 | return tree[1] 67 | } else { 68 | throw NotOKErr 69 | } 70 | } 71 | 72 | export function replaceString(body: string, original: string, replacement: string) { 73 | const regexp = new RegExp(original, 'g') // limited to principal and contract names with . and - should work 74 | return body.replace(regexp, replacement) 75 | } -------------------------------------------------------------------------------- /swapr.clarinet/Clarinet.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "swapr.clarinet" 3 | 4 | [contracts.plaid-stx-token] 5 | path = "contracts/plaid-stx-token.clar" 6 | depends_on = ["swapr-trait", "swapr"] 7 | 8 | [contracts.plaid-token] 9 | path = "contracts/plaid-token.clar" 10 | depends_on = ["sip-010"] 11 | 12 | [contracts.sip-010] 13 | path = "contracts/sip-010.clar" 14 | depends_on = [] 15 | 16 | [contracts.stx-token] 17 | path = "contracts/stx-token.clar" 18 | depends_on = ["sip-010"] 19 | 20 | [contracts.swapr] 21 | path = "contracts/swapr.clar" 22 | depends_on = ["sip-010", "swapr-trait"] 23 | 24 | [contracts.swapr-trait] 25 | path = "contracts/swapr-trait.clar" 26 | depends_on = [] 27 | -------------------------------------------------------------------------------- /swapr.clarinet/contracts/plaid-stx-token.clar: -------------------------------------------------------------------------------- 1 | ;; ;; we implement the sip-010 + a mint function 2 | (impl-trait 'ST000000000000000000002AMW42H.swapr-trait.swapr-trait) 3 | 4 | ;; ;; we can use an ft-token here, so use it! 5 | (define-fungible-token token) 6 | 7 | (define-constant no-acccess-err u40) 8 | 9 | ;; implement all functions required by sip-010 10 | 11 | (define-public (transfer (amount uint) (sender principal) (recipient principal)) 12 | (begin 13 | (ft-transfer? token amount tx-sender recipient) 14 | ) 15 | ) 16 | 17 | (define-read-only (get-name) 18 | (ok "plaid-stx-swapr") 19 | ;; (contract-call? 'ST000000000000000000002AMW42H.swapr get-name 'ST000000000000000000002AMW42H.plaid-token 'ST000000000000000000002AMW42H.stx-token) 20 | ) 21 | 22 | (define-read-only (get-symbol) 23 | (ok "plaid-stx-swapr") 24 | ;; (contract-call? 'ST000000000000000000002AMW42H.swapr get-symbol 'ST000000000000000000002AMW42H.plaid-token 'ST000000000000000000002AMW42H.stx-token) 25 | ) 26 | 27 | ;; the number of decimals used 28 | (define-read-only (get-decimals) 29 | (ok u6) ;; arbitrary, or ok? 30 | ) 31 | 32 | (define-read-only (get-balance-of (owner principal)) 33 | (ok (ft-get-balance token owner)) 34 | ) 35 | 36 | (define-read-only (get-total-supply) 37 | (ok u0) 38 | ;; (contract-call? 'ST000000000000000000002AMW42H.swapr get-total-supply 'ST000000000000000000002AMW42H.plaid-token 'ST000000000000000000002AMW42H.stx-token) 39 | ) 40 | 41 | (define-read-only (get-token-uri) 42 | (ok (some u"https://swapr.finance/tokens/plaid-stx-token.json")) 43 | ) 44 | 45 | 46 | ;; the extra mint method used by swapr 47 | ;; can only be used by swapr main contract 48 | (define-public (mint (recipient principal) (amount uint)) 49 | (begin 50 | (print "token-swapr.mint") 51 | (print contract-caller) 52 | (print amount) 53 | (if (is-eq contract-caller 'ST000000000000000000002AMW42H.swapr) 54 | (ft-mint? token amount recipient) 55 | (err no-acccess-err) 56 | ) 57 | ) 58 | ) 59 | -------------------------------------------------------------------------------- /swapr.clarinet/contracts/plaid-token.clar: -------------------------------------------------------------------------------- 1 | ;; wrap the native STX token into an SRC20 compatible token to be usable along other tokens 2 | ;; (use-trait src20-token .src20-trait.src20-trait) 3 | (impl-trait 'ST000000000000000000002AMW42H.sip-010.ft-trait) 4 | 5 | (define-fungible-token plaid) 6 | 7 | ;; get the token balance of owner 8 | (define-read-only (get-balance-of (owner principal)) 9 | (ok (ft-get-balance plaid owner)) 10 | ) 11 | 12 | ;; returns the total number of tokens 13 | ;; TODO(psq): we don't have access yet, but once POX is available, this should be a value that 14 | ;; is available from Clarity 15 | (define-read-only (get-total-supply) 16 | (ok u0) 17 | ) 18 | 19 | ;; returns the token name 20 | (define-read-only (get-name) 21 | (ok "Plaid") 22 | ) 23 | 24 | (define-read-only (get-symbol) 25 | (ok "PLD") 26 | ) 27 | 28 | ;; the number of decimals used 29 | (define-read-only (get-decimals) 30 | (ok u8) 31 | ) 32 | 33 | (define-read-only (get-token-uri) 34 | (ok (some u"https://swapr.finance/tokens/plaid.json")) 35 | ) 36 | ;; { 37 | ;; "name":"Plaid", 38 | ;; "description":"Plaid token, uses as a test token", 39 | ;; "image":"https://swapr.finance/tokens/plaid.png" 40 | ;; } 41 | 42 | 43 | ;; (transfer (uint principal principal) (response bool uint)) 44 | ;; amount sender recipient 45 | ;; Transfers tokens to a recipient 46 | (define-public (transfer (amount uint) (sender principal) (recipient principal)) 47 | (begin 48 | (print "plaid.transfer") 49 | (print amount) 50 | (print tx-sender) 51 | (print recipient) 52 | (asserts! (is-eq tx-sender sender) (err u255)) ;; too strict? 53 | (print (ft-transfer? plaid amount tx-sender recipient)) 54 | ) 55 | ) 56 | 57 | ;; (ft-mint? plaid u100000000000000 'ST3J2GVMMM2R07ZFBJDWTYEYAR8FZH5WKDTFJ9AHA) 58 | ;; (ft-mint? plaid u100000000000000 'ST1TWA18TSWGDAFZT377THRQQ451D1MSEM69C761) 59 | ;; (ft-mint? plaid u100000000000000 'ST50GEWRE7W5B02G3J3K19GNDDAPC3XPZPYQRQDW) 60 | (ft-mint? plaid u1000000000000 'ST2SVRCJJD90TER037VCSAFA781HQTCPFK9YRA6J5) ;; expected but clarinet uses ST3AA33M8SS15A30ETXE134ZXD8TNEDHT8Q955G40 61 | (ft-mint? plaid u1000000000000 'ST3AA33M8SS15A30ETXE134ZXD8TNEDHT8Q955G40) ;; for clarinet, because not the same derivation 62 | -------------------------------------------------------------------------------- /swapr.clarinet/contracts/sip-010.clar: -------------------------------------------------------------------------------- 1 | ;; sip-010 trait 2 | ;; use https://explorer.stacks.co/txid/SP3FBR2AGK5H9QBDH3EEN6DF8EK8JY7RX8QJ5SVTE.sip-10-ft-standard 3 | 4 | (define-trait ft-trait 5 | ( 6 | ;; Transfer from the caller to a new principal 7 | (transfer (uint principal principal) (response bool uint)) 8 | 9 | ;; the human readable name of the token 10 | (get-name () (response (string-ascii 32) uint)) 11 | 12 | ;; the ticker symbol, or empty if none 13 | (get-symbol () (response (string-ascii 32) uint)) 14 | 15 | ;; the number of decimals used, e.g. 6 would mean 1_000_000 represents 1 token 16 | (get-decimals () (response uint uint)) 17 | 18 | ;; the balance of the passed principal 19 | (get-balance-of (principal) (response uint uint)) 20 | 21 | ;; the current total supply (which does not need to be a constant) 22 | (get-total-supply () (response uint uint)) 23 | 24 | ;; an optional URI that represents metadata of this token 25 | (get-token-uri () (response (optional (string-utf8 256)) uint)) 26 | ) 27 | ) 28 | -------------------------------------------------------------------------------- /swapr.clarinet/contracts/stx-token.clar: -------------------------------------------------------------------------------- 1 | ;; wrap the native STX token into an SRC20 compatible token to be usable along other tokens 2 | ;; (use-trait src20-token .src20-trait.src20-trait) 3 | (impl-trait 'ST000000000000000000002AMW42H.sip-010.ft-trait) 4 | 5 | ;; get the token balance of owner 6 | (define-read-only (get-balance-of (owner principal)) 7 | (begin 8 | (ok (print (stx-get-balance owner))) 9 | ) 10 | ) 11 | 12 | ;; returns the total number of tokens 13 | ;; TODO(psq): we don't have access yet, but once POX is available, this should be a value that 14 | ;; is available from Clarity 15 | (define-read-only (get-total-supply) 16 | (ok u0) 17 | ) 18 | 19 | ;; returns the token name 20 | (define-read-only (get-name) 21 | (ok "stx") 22 | ) 23 | 24 | (define-read-only (get-symbol) 25 | (ok "STX") 26 | ) 27 | 28 | ;; the number of decimals used 29 | (define-read-only (get-decimals) 30 | (ok u6) 31 | ) 32 | 33 | (define-read-only (get-token-uri) 34 | (ok (some u"https://swapr.finance/tokens/stx.json")) 35 | ) 36 | ;; { 37 | ;; "name":"STX", 38 | ;; "description":"STX token, as a SIP-010 compatible token", 39 | ;; "image":"https://swapr.finance/tokens/stx.png" 40 | ;; } 41 | 42 | ;; Transfers tokens to a recipient 43 | (define-public (transfer (amount uint) (sender principal) (recipient principal)) 44 | (begin 45 | (print "stx.transfer") 46 | (print amount) 47 | (print tx-sender) 48 | (print recipient) 49 | (asserts! (is-eq tx-sender sender) (err u255)) ;; too strict? 50 | (print (stx-transfer? amount tx-sender recipient)) 51 | ) 52 | ) 53 | -------------------------------------------------------------------------------- /swapr.clarinet/contracts/swapr-trait.clar: -------------------------------------------------------------------------------- 1 | ;; this is an SRC20 method with an additional mint function 2 | ;; as Clarity does not support "includes", copy the needed funcitons, and add new ones 3 | 4 | (define-trait swapr-trait 5 | ( 6 | ;; Transfer from the caller to a new principal 7 | (transfer (uint principal principal) (response bool uint)) 8 | 9 | ;; the human readable name of the token 10 | (get-name () (response (string-ascii 32) uint)) 11 | 12 | ;; the ticker symbol, or empty if none 13 | (get-symbol () (response (string-ascii 32) uint)) 14 | 15 | ;; the number of decimals used, e.g. 6 would mean 1_000_000 represents 1 token 16 | (get-decimals () (response uint uint)) 17 | 18 | ;; the balance of the passed principal 19 | (get-balance-of (principal) (response uint uint)) 20 | 21 | ;; the current total supply (which does not need to be a constant) 22 | (get-total-supply () (response uint uint)) 23 | 24 | ;; an optional URI that represents metadata of this token 25 | (get-token-uri () (response (optional (string-utf8 256)) uint)) 26 | 27 | ;; additional functions specific to swapr 28 | 29 | ;; mint function only swapr contract can call 30 | (mint (principal uint) (response bool uint)) 31 | ) 32 | ) 33 | -------------------------------------------------------------------------------- /swapr.clarinet/contracts/swapr.clar: -------------------------------------------------------------------------------- 1 | (use-trait src20-token 'ST000000000000000000002AMW42H.sip-010.ft-trait) 2 | (use-trait swapr-token 'ST000000000000000000002AMW42H.swapr-trait.swapr-trait) 3 | 4 | (define-constant contract-owner 'ST3J2GVMMM2R07ZFBJDWTYEYAR8FZH5WKDTFJ9AHA) 5 | (define-constant no-liquidity-err (err u61)) 6 | (define-constant transfer-failed-err (err u62)) 7 | (define-constant not-owner-err (err u63)) 8 | (define-constant no-fee-to-address-err (err u64)) 9 | (define-constant invalid-pair-err (err u65)) 10 | (define-constant no-such-position-err (err u66)) 11 | (define-constant balance-too-low-err (err u67)) 12 | (define-constant too-many-pairs-err (err u68)) 13 | (define-constant pair-already-exists-err (err u69)) 14 | (define-constant wrong-token-err (err u70)) 15 | (define-constant too-much-slippage-err (err u71)) 16 | 17 | ;; for future use, or debug 18 | (define-constant e10-err (err u20)) 19 | (define-constant e11-err (err u21)) 20 | (define-constant e12-err (err u22)) 21 | 22 | ;; ;; V1 23 | ;; ;; overall balance of x-token and y-token held by the contract 24 | ;; (define-data-var balance-x uint u0) 25 | ;; (define-data-var balance-y uint u0) 26 | 27 | ;; ;; fees collected so far, that have not been withdrawn (saves gas while doing exchanges) 28 | ;; (define-data-var fee-balance-x uint u0) 29 | ;; (define-data-var fee-balance-y uint u0) 30 | 31 | ;; ;; balances help by all the clients holding shares, this is equal to the sum of all the balances held in shares by each client 32 | ;; (define-data-var total-balances uint u0) 33 | ;; (define-map shares 34 | ;; ((owner principal)) 35 | ;; ((balance uint)) 36 | ;; ) 37 | 38 | ;; ;; when set, enables the fee, and provides whene to send the fee when calling collect-fees 39 | ;; (define-data-var fee-to-address (optional principal) none) 40 | 41 | ;; V2 42 | ;; variables 43 | ;; (name) => (token-x, token-y) 44 | ;; (token-x, token-y) => (shares-total, balance-x, balance-y, fee-balance-x, fee-balance-y, fee-to-address) 45 | ;; (token-x, token-y, principal) => (shares) 46 | 47 | (define-map pairs-map 48 | { pair-id: uint } 49 | { 50 | token-x: principal, 51 | token-y: principal, 52 | } 53 | ) 54 | 55 | (define-map pairs-data-map 56 | { 57 | token-x: principal, 58 | token-y: principal, 59 | } 60 | { 61 | shares-total: uint, 62 | fee-balance-x: uint, 63 | fee-balance-y: uint, 64 | fee-to-address: (optional principal), 65 | swapr-token: principal, 66 | name: (string-ascii 32), 67 | } 68 | ) 69 | 70 | ;; ;; TODO(psq): replace use of balance-x/balance-y with a call to balance-of(swapr) on the token itself, no write to do actually!!! The transfer is the write, that's cool :) 71 | 72 | 73 | ;; (define-map shares-map 74 | ;; ((token-x principal) (token-y principal) (owner principal)) 75 | ;; ((shares uint)) 76 | ;; ) 77 | 78 | ;; (define-data-var pairs-list (list 2000 uint) (list)) 79 | (define-data-var pair-count uint u0) 80 | 81 | 82 | ;; token support, these ~~4~~3 calls need to be wrapped in a contract that hardcodes the 2 pairs to present 83 | ;; a unified, src20 compatible trait 84 | 85 | ;; (define-public (transfer (token-x-trait ) (token-y-trait ) (recipient principal) (amount uint) (token-trait )) 86 | ;; (let ((token-x (contract-of token-x-trait)) (token-y (contract-of token-y-trait)) (token (contract-of token-trait))) 87 | ;; (let ((pair (unwrap! (map-get? pairs-data-map { token-x: token-x, token-y: token-y }) invalid-pair-err))) 88 | ;; (if (eq token (get swapr-token pair)) 89 | ;; (contract-call? token-trait transfer recipient amount) 90 | ;; (err wrong-token-err) 91 | ;; ) 92 | ;; ) 93 | ;; ) 94 | ;; ) 95 | 96 | (define-read-only (get-name (token-x-trait ) (token-y-trait )) 97 | (let ((token-x (contract-of token-x-trait)) (token-y (contract-of token-y-trait))) 98 | (let ((pair (unwrap! (map-get? pairs-data-map { token-x: token-x, token-y: token-y }) invalid-pair-err))) 99 | (ok (get name pair)) 100 | ) 101 | ) 102 | ) 103 | 104 | (define-public (get-symbol (token-x-trait ) (token-y-trait )) 105 | ;; TODO(psq): this should be the symbol of the pair, not a single token 106 | ;; TODO(psq): obvious not the rigth thing to do here 107 | ;; (contract-call? 'ST3J2GVMMM2R07ZFBJDWTYEYAR8FZH5WKDTFJ9AHA.plaid-token symbol) 108 | (contract-call? token-y-trait get-symbol) 109 | ;; (ok (concat (unwrap-panic (as-max-len? (unwrap-panic (contract-call? token-x-trait symbol)) u15)) (concat "-" (unwrap-panic (as-max-len? (unwrap-panic (contract-call? token-y-trait symbol)) u15))))) 110 | ) 111 | 112 | ;; (define-public (balance-of (token-x-trait ) (token-y-trait ) (owner principal)) 113 | ;; (begin 114 | ;; (let ((token-x (contract-of token-x-trait)) (token-y (contract-of token-y-trait))) 115 | ;; (print "swapr.balance-of") 116 | ;; (print token-x) 117 | ;; (print token-y) 118 | ;; (print owner) 119 | ;; (ok (print (shares-of token-x token-y owner))) 120 | ;; ) 121 | ;; ) 122 | ;; ) 123 | 124 | (define-read-only (get-total-supply (token-x-trait ) (token-y-trait )) 125 | (let ((token-x (contract-of token-x-trait)) (token-y (contract-of token-y-trait))) 126 | (let ((pair (unwrap! (map-get? pairs-data-map { token-x: token-x, token-y: token-y }) invalid-pair-err))) 127 | (ok (get shares-total pair)) 128 | ) 129 | ) 130 | ) 131 | 132 | 133 | ;; wrappers to get an owner's position 134 | ;; (define-private (shares-of (token-x principal) (token-y principal) (owner principal)) 135 | ;; (default-to u0 136 | ;; (get shares 137 | ;; (map-get? shares-map ((token-x token-x) (token-y token-y) (owner owner))) 138 | ;; ) 139 | ;; ) 140 | ;; ) 141 | 142 | ;; get the number of shares of the pool for owner 143 | ;; (define-read-only (get-shares-of (token-x principal) (token-y principal) (owner principal)) 144 | ;; (ok (shares-of token-x token-y owner)) 145 | ;; ) 146 | 147 | ;; get the total number of shares in the pool 148 | (define-read-only (get-shares (token-x principal) (token-y principal)) 149 | (ok (get shares-total (unwrap! (map-get? pairs-data-map { token-x: token-x, token-y: token-y }) invalid-pair-err))) 150 | ) 151 | 152 | ;; TODO(psq): only works if a token is in only one pair, to make this work, needs a new instance of the contract per pair at a different address 153 | ;; which would remove the need for a separate token, as swapr can be its own token 154 | (define-private (get-balance (token-trait )) 155 | (begin 156 | (unwrap-panic (contract-call? token-trait get-balance-of (as-contract tx-sender))) 157 | ) 158 | ) 159 | 160 | ;; (define-public (get-balances-of (token-x-trait ) (token-y-trait ) (owner principal)) 161 | ;; (let ((token-x (contract-of token-x-trait)) (token-y (contract-of token-y-trait))) 162 | ;; (let ((pair (unwrap! (map-get? pairs-data-map { token-x: token-x, token-y: token-y }) invalid-pair-err))) 163 | ;; (let ((x (balance token-x-trait)) (y (balance token-y-trait)) (shares-total (get shares-total pair)) (shares (shares-of token-x token-y owner))) 164 | ;; (if (> shares-total u0) 165 | ;; (ok (list (/ (* x shares) shares-total) (/ (* y shares) shares-total))) ;; less precision loss doing * first 166 | ;; no-liquidity-err ;; no liquidity 167 | ;; ) 168 | ;; ) 169 | ;; ) 170 | ;; ) 171 | ;; ) 172 | 173 | ;; (define-private (increase-shares (token-x principal) (token-y principal) (owner principal) (amount uint)) 174 | ;; (let ((shares (shares-of token-x token-y owner))) 175 | ;; (print "swapr.increase-shares") 176 | ;; (print token-x) 177 | ;; (print token-y) 178 | ;; (print owner) 179 | ;; (print shares) 180 | ;; (print amount) 181 | ;; (print (map-set shares-map 182 | ;; ((token-x token-x) (token-y token-y) (owner owner)) 183 | ;; ((shares (+ shares amount))) 184 | ;; )) 185 | ;; (ok true) 186 | ;; ) 187 | ;; ) 188 | 189 | ;; (define-private (decrease-shares (token-x principal) (token-y principal) (owner principal) (amount uint)) 190 | ;; (let ((shares (shares-of token-x token-y owner))) 191 | ;; (if (< amount shares) 192 | ;; (begin 193 | ;; (map-set shares-map 194 | ;; ((token-x token-x) (token-y token-y) (owner owner)) 195 | ;; ((shares (- shares amount))) 196 | ;; ) 197 | ;; (ok true) 198 | ;; ) 199 | ;; balance-too-low-err 200 | ;; ) 201 | ;; ) 202 | ;; ) 203 | 204 | ;; get overall balances for the pair 205 | (define-public (get-balances (token-x-trait ) (token-y-trait )) 206 | (let ((token-x (contract-of token-x-trait)) (token-y (contract-of token-y-trait))) 207 | (let ((pair (unwrap! (map-get? pairs-data-map { token-x: token-x, token-y: token-y }) invalid-pair-err))) 208 | (ok (list (get-balance token-x-trait) (get-balance token-y-trait))) 209 | ) 210 | ) 211 | ) 212 | 213 | ;; since we can't use a constant to refer to contract address, here what x and y are 214 | ;; (define-constant x-token 'SP2NC4YKZWM2YMCJV851VF278H9J50ZSNM33P3JM1.my-token) 215 | ;; (define-constant y-token 'SP1QR3RAGH3GEME9WV7XB0TZCX6D5MNDQP97D35EH.my-token) 216 | (define-public (add-to-position (token-x-trait ) (token-y-trait ) (token-swapr-trait ) (x uint) (y uint)) 217 | (let ((token-x (contract-of token-x-trait)) (token-y (contract-of token-y-trait))) 218 | (let ((pair (unwrap! (map-get? pairs-data-map { token-x: token-x, token-y: token-y }) invalid-pair-err)) (contract-address (as-contract tx-sender)) (recipient-address tx-sender)) 219 | ;; TODO(psq) check if x or y is 0, to calculate proper exchange rate unless shares-total is 0, which would be an error 220 | (if 221 | (and 222 | ;; TODO(psq): check that the amount transfered in matches the amount requested 223 | (is-ok (contract-call? token-x-trait transfer x tx-sender contract-address)) 224 | (is-ok (contract-call? token-y-trait transfer y tx-sender contract-address)) 225 | ) 226 | (begin 227 | (print "calculate new-shares") 228 | (let ((new-shares (if (is-eq (print (get shares-total pair)) u0) 229 | (let ((shares (sqrti (* x y)))) 230 | ;; (increase-shares token-x token-y tx-sender shares) 231 | shares 232 | ) 233 | (let ((shares (/ (* (print x) (print (get shares-total pair))) (print (get-balance token-x-trait))))) 234 | ;; (increase-shares token-x token-y tx-sender shares) 235 | shares 236 | ) 237 | ))) 238 | (map-set pairs-data-map { token-x: token-x, token-y: token-y } 239 | { 240 | shares-total: (+ new-shares (get shares-total pair)), 241 | fee-balance-x: (get fee-balance-x pair), 242 | fee-balance-y: (get fee-balance-y pair), 243 | fee-to-address: (get fee-to-address pair), 244 | name: (get name pair), 245 | swapr-token: (get swapr-token pair), 246 | } 247 | ) 248 | (print "token-swapr-trait.mint params") 249 | (print x) 250 | (print y) 251 | (print recipient-address) 252 | (print new-shares) 253 | (print (contract-call? token-swapr-trait mint recipient-address new-shares)) 254 | ) 255 | ) 256 | (begin 257 | transfer-failed-err 258 | ) 259 | ) 260 | ) 261 | ) 262 | ) 263 | 264 | (define-read-only (get-pair-details (token-x principal) (token-y principal)) 265 | (unwrap-panic (map-get? pairs-data-map { token-x: token-x, token-y: token-y })) 266 | ) 267 | 268 | (define-read-only (get-pair-contracts (pair-id uint)) 269 | (unwrap-panic (map-get? pairs-map { pair-id: pair-id })) 270 | ) 271 | 272 | (define-read-only (get-pair-count) 273 | (ok (var-get pair-count)) 274 | ) 275 | 276 | ;; (define-read-only (get-pairs) 277 | ;; (ok (map get-pair-contracts (var-get pairs-list))) 278 | ;; ) 279 | 280 | (define-public (create-pair (token-x-trait ) (token-y-trait ) (token-swapr-trait ) (pair-name (string-ascii 32)) (x uint) (y uint)) 281 | ;; TOOD(psq): add creation checks, then create map before proceeding to add-to-position 282 | ;; check neither x,y or y,x exists` 283 | (let ((name-x (unwrap-panic (contract-call? token-x-trait get-name))) (name-y (unwrap-panic (contract-call? token-y-trait get-name)))) 284 | (let ((token-x (contract-of token-x-trait)) (token-y (contract-of token-y-trait)) (pair-id (+ (var-get pair-count) u1))) 285 | (if (and (is-none (map-get? pairs-data-map { token-x: token-x, token-y: token-y })) (is-none (map-get? pairs-data-map { token-x: token-y, token-y: token-x }))) 286 | (begin 287 | (map-set pairs-data-map { token-x: token-x, token-y: token-y } 288 | { 289 | shares-total: u0, 290 | fee-balance-x: u0, 291 | fee-balance-y: u0, 292 | fee-to-address: none, 293 | swapr-token: (contract-of token-swapr-trait), 294 | name: pair-name, 295 | } 296 | ) 297 | (map-set pairs-map { pair-id: pair-id } { token-x: token-x, token-y: token-y }) 298 | ;; (var-set pairs-list (unwrap! (as-max-len? (append (var-get pairs-list) pair-id) u2000) too-many-pairs-err)) 299 | (var-set pair-count pair-id) 300 | (add-to-position token-x-trait token-y-trait token-swapr-trait x y) 301 | ) 302 | pair-already-exists-err 303 | ) 304 | ) 305 | ) 306 | ) 307 | 308 | 309 | 310 | ;; ;; reduce the amount of liquidity the sender provides to the pool 311 | ;; ;; to close, use u100 312 | (define-public (reduce-position (token-x-trait ) (token-y-trait ) (token-swapr-trait ) (percent uint)) 313 | (let ((token-x (contract-of token-x-trait)) (token-y (contract-of token-y-trait))) 314 | (let ((pair (unwrap! (map-get? pairs-data-map { token-x: token-x, token-y: token-y }) invalid-pair-err))) 315 | (let ((shares (unwrap-panic (contract-call? token-swapr-trait get-balance-of tx-sender))) (shares-total (get shares-total pair)) (contract-address (as-contract tx-sender)) (sender tx-sender)) 316 | (let ((withdrawal (/ (* shares percent) u100))) 317 | (let ((withdrawal-x (/ (* withdrawal (get-balance token-x-trait)) shares-total)) (withdrawal-y (/ (* withdrawal (get-balance token-y-trait)) shares-total))) 318 | (if 319 | (and 320 | (<= percent u100) 321 | (is-ok (as-contract (contract-call? token-x-trait transfer withdrawal-x contract-address sender))) 322 | (is-ok (as-contract (contract-call? token-y-trait transfer withdrawal-y contract-address sender))) 323 | ) 324 | (begin 325 | ;; (unwrap-panic (decrease-shares token-x token-y tx-sender withdrawal)) ;; should never fail, you know... 326 | (map-set pairs-data-map { token-x: token-x, token-y: token-y } 327 | { 328 | shares-total: (- shares-total withdrawal), 329 | fee-balance-x: (get fee-balance-x pair), 330 | fee-balance-y: (get fee-balance-y pair), 331 | fee-to-address: (get fee-to-address pair), 332 | name: (get name pair), 333 | swapr-token: (get swapr-token pair), 334 | } 335 | ) 336 | ;; TODO(psq): use burn 337 | (unwrap-panic (contract-call? token-swapr-trait transfer withdrawal tx-sender contract-address)) ;; transfer back to swapr, wish there was a burn instead... 338 | (ok (list withdrawal-x withdrawal-y)) 339 | ) 340 | transfer-failed-err 341 | ) 342 | ) 343 | ) 344 | ) 345 | ) 346 | ) 347 | ) 348 | 349 | ;; exchange known dx of x-token for whatever dy of y-token based on current liquidity, returns (dx dy) 350 | ;; the swap will not happen if can't get at least min-dy back 351 | (define-public (swap-exact-x-for-y (token-x-trait ) (token-y-trait ) (dx uint) (min-dy uint)) 352 | ;; calculate dy 353 | ;; calculate fee on dx 354 | ;; transfer 355 | ;; update balances 356 | (let ((token-x (contract-of token-x-trait)) (token-y (contract-of token-y-trait))) 357 | (let ((pair (unwrap! (map-get? pairs-data-map { token-x: token-x, token-y: token-y }) invalid-pair-err))) 358 | (let 359 | ( 360 | (contract-address (as-contract tx-sender)) 361 | (sender tx-sender) 362 | (dy (/ (* u997 (get-balance token-y-trait) dx) (+ (* u1000 (get-balance token-x-trait)) (* u997 dx)))) ;; overall fee is 30 bp, either all for the pool, or 25 bp for pool and 5 bp for operator 363 | (fee (/ (* u5 dx) u10000)) ;; 5 bp 364 | ) 365 | (if (and 366 | ;; TODO(psq): check that the amount transfered in matches the amount requested 367 | (is-ok (contract-call? token-x-trait transfer dx sender contract-address)) 368 | (is-ok (as-contract (contract-call? token-y-trait transfer dy contract-address sender))) 369 | ) 370 | (begin 371 | (map-set pairs-data-map { token-x: token-x, token-y: token-y } 372 | { 373 | shares-total: (get shares-total pair), 374 | fee-balance-x: 375 | (if (is-some (get fee-to-address pair)) ;; only collect fee when fee-to-address is set 376 | (+ fee (get fee-balance-x pair)) 377 | (get fee-balance-x pair) 378 | ) 379 | , 380 | fee-balance-y: (get fee-balance-y pair), 381 | fee-to-address: (get fee-to-address pair), 382 | name: (get name pair), 383 | swapr-token: (get swapr-token pair), 384 | } 385 | ) 386 | (ok (list dx dy)) 387 | ) 388 | transfer-failed-err 389 | ) 390 | ) 391 | ) 392 | ) 393 | ) 394 | 395 | ;; exchange known dy for whatever dx based on liquidity, returns (dx dy) 396 | ;; the swap will not happen if can't get at least min-dx back 397 | (define-public (swap-exact-y-for-x (token-x-trait ) (token-y-trait ) (dy uint) (min-dx uint)) 398 | ;; calculate dx 399 | ;; calculate fee on dy 400 | ;; transfer 401 | ;; update balances 402 | (let ((token-x (contract-of token-x-trait)) (token-y (contract-of token-y-trait))) 403 | (let ((pair (unwrap! (map-get? pairs-data-map { token-x: token-x, token-y: token-y }) invalid-pair-err))) 404 | (let 405 | ( 406 | (contract-address (as-contract tx-sender)) 407 | (sender tx-sender) 408 | (dx (/ (* u997 (get-balance token-x-trait) dy) (+ (* u1000 (get-balance token-y-trait)) (* u997 dy)))) ;; overall fee is 30 bp, either all for the pool, or 25 bp for pool and 5 bp for operator 409 | (fee (/ (* u5 dy) u10000)) ;; 5 bp 410 | ) 411 | (if (and 412 | ;; TODO(psq): check that the amount transfered in matches the amount requested 413 | (is-ok (as-contract (contract-call? token-x-trait transfer dx contract-address sender))) 414 | (is-ok (contract-call? token-y-trait transfer dy sender contract-address)) 415 | ) 416 | (begin 417 | (map-set pairs-data-map { token-x: token-x, token-y: token-y } 418 | { 419 | shares-total: (get shares-total pair), 420 | ;; (balance-x (- (balance token-x-trait) dx)) ;; remove dx 421 | ;; (balance-y 422 | ;; (if (is-some (get fee-to-address pair)) ;; only collect fee when fee-to-address is set 423 | ;; (- (+ (balance token-y-trait) dy) fee) ;; add dy - fee 424 | ;; (+ (balance token-y-trait) dy) ;; add dy 425 | ;; ) 426 | ;; ) 427 | fee-balance-x: (get fee-balance-x pair), 428 | fee-balance-y: 429 | (if (is-some (get fee-to-address pair)) ;; only collect fee when fee-to-address is set 430 | (+ fee (get fee-balance-y pair)) 431 | (get fee-balance-y pair) 432 | ) 433 | , 434 | fee-to-address: (get fee-to-address pair), 435 | name: (get name pair), 436 | swapr-token: (get swapr-token pair), 437 | } 438 | ) 439 | (ok (list dx dy)) 440 | ) 441 | transfer-failed-err 442 | ) 443 | ) 444 | ) 445 | ) 446 | ) 447 | 448 | ;; ;; activate the contract fee for swaps by setting the collection address, restricted to contract owner 449 | (define-public (set-fee-to-address (token-x principal) (token-y principal) (address principal)) 450 | (let ((pair (unwrap! (map-get? pairs-data-map { token-x: token-x, token-y: token-y }) invalid-pair-err))) 451 | (if (is-eq tx-sender contract-owner) 452 | (begin 453 | (map-set pairs-data-map { token-x: token-x, token-y: token-y } 454 | { 455 | shares-total: (get shares-total pair), 456 | ;; (balance-x (balance token-x-trait)) 457 | ;; (balance-y (balance token-y-trait)) 458 | fee-balance-x: (get fee-balance-y pair), 459 | fee-balance-y: (get fee-balance-y pair), 460 | fee-to-address: (some address), 461 | name: (get name pair), 462 | swapr-token: (get swapr-token pair), 463 | } 464 | ) 465 | (ok true) 466 | ) 467 | not-owner-err 468 | ) 469 | ) 470 | ) 471 | 472 | ;; ;; clear the contract fee addres 473 | ;; ;; TODO(psq): if there are any collected fees, prevent this from happening, as the fees can no longer be collect with `collect-fees` 474 | (define-public (reset-fee-to-address (token-x principal) (token-y principal)) 475 | (let ((pair (unwrap! (map-get? pairs-data-map { token-x: token-x, token-y: token-y }) invalid-pair-err))) 476 | (if (is-eq tx-sender contract-owner) 477 | (begin 478 | (map-set pairs-data-map { token-x: token-x, token-y: token-y } 479 | { 480 | shares-total: (get shares-total pair), 481 | ;; (balance-x (balance token-x-trait)) 482 | ;; (balance-y (balance token-y-trait)) 483 | fee-balance-x: (get fee-balance-y pair), 484 | fee-balance-y: (get fee-balance-y pair), 485 | fee-to-address: none, 486 | name: (get name pair), 487 | swapr-token: (get swapr-token pair), 488 | } 489 | ) 490 | (ok true) 491 | ) 492 | not-owner-err 493 | ) 494 | ) 495 | ) 496 | 497 | ;; ;; get the current address used to collect a fee 498 | (define-read-only (get-fee-to-address (token-x principal) (token-y principal)) 499 | (let ((pair (unwrap! (map-get? pairs-data-map { token-x: token-x, token-y: token-y }) invalid-pair-err))) 500 | (ok (get fee-to-address pair)) 501 | ) 502 | ) 503 | 504 | ;; ;; get the amount of fees charged on x-token and y-token exchanges that have not been collected yet 505 | (define-read-only (get-fees (token-x principal) (token-y principal)) 506 | (let ((pair (unwrap! (map-get? pairs-data-map { token-x: token-x, token-y: token-y }) invalid-pair-err))) 507 | (ok (list (get fee-balance-x pair) (get fee-balance-y pair))) 508 | ) 509 | ) 510 | 511 | ;; ;; send the collected fees the fee-to-address 512 | (define-public (collect-fees (token-x-trait ) (token-y-trait )) 513 | (let ((token-x (contract-of token-x-trait)) (token-y (contract-of token-y-trait)) (contract-address (as-contract tx-sender))) 514 | (let ((pair (unwrap! (map-get? pairs-data-map { token-x: token-x, token-y: token-y }) invalid-pair-err))) 515 | (let ((address (unwrap! (get fee-to-address pair) no-fee-to-address-err)) (fee-x (get fee-balance-x pair)) (fee-y (get fee-balance-y pair))) 516 | (if 517 | (and 518 | (or (is-eq fee-x u0) (is-ok (as-contract (contract-call? token-x-trait transfer fee-x contract-address address)))) 519 | (or (is-eq fee-y u0) (is-ok (as-contract (contract-call? token-y-trait transfer fee-y contract-address address)))) 520 | ) 521 | (begin 522 | (map-set pairs-data-map { token-x: token-x, token-y: token-y } 523 | { 524 | shares-total: (get shares-total pair), 525 | ;; (balance-x (balance token-x-trait)) 526 | ;; (balance-y (balance token-y-trait)) 527 | fee-balance-x: u0, 528 | fee-balance-y: u0, 529 | fee-to-address: (get fee-to-address pair), 530 | name: (get name pair), 531 | swapr-token: (get swapr-token pair), 532 | } 533 | ) 534 | (ok (list fee-x fee-y)) 535 | ) 536 | transfer-failed-err 537 | ) 538 | ) 539 | ) 540 | ) 541 | ) 542 | -------------------------------------------------------------------------------- /swapr.clarinet/history.txt: -------------------------------------------------------------------------------- 1 | #V2 2 | ::help 3 | ::set_tx_sender ST3AA33M8SS15A30ETXE134ZXD8TNEDHT8Q955G40 4 | ::set_tx_sender 5 | ::get_accounts 6 | -------------------------------------------------------------------------------- /swapr.clarinet/settings/Development.toml: -------------------------------------------------------------------------------- 1 | [network] 2 | name = "Development" 3 | 4 | [accounts.wallet_1] 5 | mnemonic = "market ocean tortoise venue vivid coach machine category conduct enable insect jump fog file test core book chaos crucial burst version curious prosper fever" 6 | balance = 1_000_000_000_000 7 | 8 | [accounts.wallet_2] 9 | mnemonic = "firm sword marriage reveal please kitten salon waste enjoy desert survey bench wonder hollow endless work senior rough boring mind toast sea obscure release" 10 | balance = 1_000_000_000_000 11 | -------------------------------------------------------------------------------- /swapr.clarinet/tests/plaid-stx-token_test.js: -------------------------------------------------------------------------------- 1 | 2 | import { Clarinet, Tx, types } from 'https://deno.land/x/clarinet@v0.3.0/index.ts'; 3 | import { assertEquals } from 'https://deno.land/std@0.90.0/testing/asserts.ts'; 4 | 5 | Clarinet.test({ 6 | name: "Ensure that <...> - plaid-stx-token", 7 | async fn(chain, accounts) { 8 | let block = chain.mineBlock([ 9 | /* 10 | * Add transactions with: 11 | * Tx.contractCall(...) 12 | */ 13 | ]); 14 | assertEquals(block.receipts.length, 0); 15 | assertEquals(block.height, 2); 16 | 17 | block = chain.mineBlock([ 18 | /* 19 | * Add transactions with: 20 | * Tx.contractCall(...) 21 | */ 22 | ]); 23 | assertEquals(block.receipts.length, 0); 24 | assertEquals(block.height, 3); 25 | 26 | console.log("===", chain.callReadOnlyFn("stx-token", "get-total-supply", [], accounts[0].address)); 27 | 28 | }, 29 | }); 30 | -------------------------------------------------------------------------------- /swapr.clarinet/tests/plaid-token_test.js: -------------------------------------------------------------------------------- 1 | 2 | import { Clarinet, Tx, types } from 'https://deno.land/x/clarinet@v0.3.0/index.ts'; 3 | import { assertEquals } from 'https://deno.land/std@0.90.0/testing/asserts.ts'; 4 | 5 | Clarinet.test({ 6 | name: "Ensure that <...> - plaid-token", 7 | async fn(chain, accounts) { 8 | let block = chain.mineBlock([ 9 | /* 10 | * Add transactions with: 11 | * Tx.contractCall(...) 12 | */ 13 | ]); 14 | assertEquals(block.receipts.length, 0); 15 | assertEquals(block.height, 2); 16 | 17 | block = chain.mineBlock([ 18 | /* 19 | * Add transactions with: 20 | * Tx.contractCall(...) 21 | */ 22 | ]); 23 | assertEquals(block.receipts.length, 0); 24 | assertEquals(block.height, 3); 25 | }, 26 | }); 27 | -------------------------------------------------------------------------------- /swapr.clarinet/tests/stx-token_test.js: -------------------------------------------------------------------------------- 1 | 2 | import { Clarinet, Tx, types } from 'https://deno.land/x/clarinet@v0.3.0/index.ts'; 3 | import { assertEquals } from 'https://deno.land/std@0.90.0/testing/asserts.ts'; 4 | 5 | Clarinet.test({ 6 | name: "Ensure that <...> - stx-token", 7 | async fn(chain, accounts) { 8 | let block = chain.mineBlock([ 9 | /* 10 | * Add transactions with: 11 | * Tx.contractCall(...) 12 | */ 13 | ]); 14 | assertEquals(block.receipts.length, 0); 15 | assertEquals(block.height, 2); 16 | 17 | block = chain.mineBlock([ 18 | /* 19 | * Add transactions with: 20 | * Tx.contractCall(...) 21 | */ 22 | ]); 23 | assertEquals(block.receipts.length, 0); 24 | assertEquals(block.height, 3); 25 | 26 | console.log("get-total-supply", chain.callReadOnlyFn("stx-token", "get-total-supply", [], accounts[0].address)); 27 | 28 | }, 29 | }); 30 | -------------------------------------------------------------------------------- /swapr.clarinet/tests/swapr_test.js: -------------------------------------------------------------------------------- 1 | 2 | import { Clarinet, Tx, types } from 'https://deno.land/x/clarinet@v0.3.0/index.ts' 3 | import { assertEquals } from 'https://deno.land/std@0.90.0/testing/asserts.ts' 4 | import { unwrapTuple, parse } from './utils.js' 5 | 6 | Clarinet.test({ 7 | name: "Ensure that <...> - swapr", 8 | async fn(chain, accounts) { 9 | // console.log("types", types) 10 | let block = chain.mineBlock([ 11 | 12 | // (define-public (create-pair (token-x-trait ) (token-y-trait ) (token-swapr-trait ) (pair-name (string-ascii 32)) (x uint) (y uint)) 13 | // Tx.contractCall("counter", "increment", [types.uint(1)], accounts[0].address), 14 | 15 | 16 | Tx.contractCall('swapr', 'create-pair', [ 17 | types.principal('ST000000000000000000002AMW42H.plaid-token'), 18 | types.principal('ST000000000000000000002AMW42H.stx-token'), 19 | types.principal('ST000000000000000000002AMW42H.plaid-stx-token'), 20 | types.ascii('plaid-stx-token'), 21 | types.uint(10000000), 22 | types.uint(10000000), 23 | ], accounts[0].address) 24 | 25 | ]) 26 | // console.log("receipts", JSON.stringify(block.receipts, null, 2)) 27 | assertEquals(block.receipts.length, 1) 28 | assertEquals(block.height, 2) 29 | 30 | console.log("get-pair-count", JSON.stringify(chain.callReadOnlyFn("swapr", "get-pair-count", [], accounts[0].address), null, 2)) 31 | const result = chain.callReadOnlyFn("swapr", "get-pair-contracts", [types.uint(1)], accounts[0].address).result 32 | 33 | console.log("get-pair-contracts", JSON.stringify(result, null, 2)) 34 | console.log(unwrapTuple(parse(result))) 35 | 36 | block = chain.mineBlock([ 37 | /* 38 | * Add transactions with: 39 | * Tx.contractCall(...) 40 | */ 41 | ]) 42 | assertEquals(block.receipts.length, 0) 43 | assertEquals(block.height, 3) 44 | }, 45 | }) 46 | -------------------------------------------------------------------------------- /swapr.clarinet/tests/utils.js: -------------------------------------------------------------------------------- 1 | export function parse(value) { 2 | let index = 0 3 | 4 | function sub() { 5 | const keywords = [] 6 | let current = index 7 | 8 | function saveKeyword() { 9 | if (index - 1 > current) { 10 | const keyword = value.slice(current, index - 1) 11 | // console.log("keyword", keyword) 12 | keywords.push(keyword) 13 | } 14 | } 15 | 16 | while (index < value.length) { 17 | const c = value[index++] 18 | // console.log("c", c, index) 19 | if (c === '(') { 20 | keywords.push(sub()) 21 | current = index 22 | } else if (c === ')') { 23 | saveKeyword() 24 | return keywords 25 | } else if (c === ' ') { 26 | saveKeyword() 27 | current = index 28 | } 29 | } 30 | saveKeyword() 31 | // console.log("keywords so far", keywords) 32 | return keywords 33 | } 34 | return sub()[0] 35 | } 36 | 37 | export function unwrapTuple(tree) { 38 | // console.log("unwrapTuple", tree) 39 | if (tree[0] === 'tuple') { 40 | const result = {} 41 | result[tree[1][0]] = tree[1][1] 42 | result[tree[2][0]] = tree[2][1] 43 | return result 44 | } 45 | return null 46 | } 47 | 48 | // export function unwrapList(tree) { 49 | // // console.log("unwrapList", tree) 50 | // return tree 51 | // } 52 | 53 | // export function unwrapXYList(tree) { 54 | // // console.log("unwrapXYList", tree) 55 | // return { 56 | // x: parseInt(tree[0].substring(1)), 57 | // y: parseInt(tree[1].substring(1)), 58 | // } 59 | // } 60 | 61 | export function unwrapSome(tree) { 62 | // console.log("unwrapSome", tree) 63 | if (tree[0] === 'some') { 64 | return tree[1] 65 | } else { 66 | // throw NotSomeErr 67 | return null 68 | } 69 | } 70 | 71 | export function unwrapOK(tree) { 72 | // console.log("unwrapOK", tree) 73 | if (tree[0] === 'ok') { 74 | return tree[1] 75 | } else { 76 | // throw NotOKErr 77 | return null 78 | } 79 | } 80 | 81 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --require ts-node/register/transpile-only 2 | --require source-map-support/register 3 | --extension ts,clar 4 | --timeout 0 5 | --recursive 6 | -------------------------------------------------------------------------------- /test/unit/flexr.ts: -------------------------------------------------------------------------------- 1 | import { Client, Provider, ProviderRegistry, Result } from "@blockstack/clarity" 2 | import { readFileSync } from 'fs' 3 | 4 | const chai = require('chai') 5 | chai.use(require('chai-string')) 6 | const assert = chai.assert 7 | 8 | import { providerWithInitialAllocations } from "./providerWithInitialAllocations"; 9 | 10 | import { FlexrClient } from "../../src/clients/flexr-client" 11 | import { GeyserClient } from "../../src/clients/geyser-client" 12 | import { OracleClient } from "../../src/clients/oracle-client" 13 | import { SwaprClient } from "../../src/clients/swapr-client" 14 | import { FlexrStxTokenClient } from "../../src/clients/flexr-stx-token-client" 15 | import { PlaidStxTokenClient } from "../../src/clients/plaid-stx-token-client" 16 | import { StxClient } from "../../src/clients/stx-client" 17 | import { PlaidClient } from "../../src/clients/plaid-client" 18 | import { 19 | NoLiquidityError, 20 | NotOKErr, 21 | NotOwnerError, 22 | TransferError, 23 | } from '../../src/errors' 24 | 25 | import * as balances from '../../balances.json' 26 | 27 | console.log(balances) 28 | 29 | describe("full test suite", () => { 30 | let provider: Provider 31 | 32 | let src20TraitClient: Client 33 | let swaprTraitClient: Client 34 | 35 | let flexrClient: Client 36 | let geyserClient: Client 37 | let oracleClient: Client 38 | let swaprClient: Client 39 | let flexrStxTokenClient: Client 40 | let stxClient: Client 41 | let plaidClient: Client 42 | let plaidStxTokenClient: Client 43 | 44 | const prices = [ 45 | 1_100_000, 46 | 1_150_000, 47 | 1_050_000, 48 | 950_000, 49 | 900_000, 50 | 1_000_000, 51 | 1_000_000, 52 | 1_000_000, 53 | ] 54 | 55 | const addresses = [ 56 | "SP2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKNRV9EJ7", // alice, u20 tokens of each 57 | "S02J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKPVKG2CE", // bob, u10 tokens of each 58 | "SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR", // zoe, no tokens 59 | "SP138CBPVKYBQQ480EZXJQK89HCHY32XBQ0T4BCCD", // TBD 60 | "SP30JX68J79SMTTN0D2KXQAJBFVYY56BZJEYS3X0B", // flexr treasury 61 | 62 | ] 63 | const alice = addresses[0] 64 | const bob = addresses[1] 65 | const zoe = addresses[2] 66 | const flexr_treasury = `${addresses[4]}` 67 | const flexr_token = `ST3J2GVMMM2R07ZFBJDWTYEYAR8FZH5WKDTFJ9AHA.flexr-token` 68 | const flexr_stx_token = `ST3J2GVMMM2R07ZFBJDWTYEYAR8FZH5WKDTFJ9AHA.flexr-stx-token` 69 | const stx_token = `ST3J2GVMMM2R07ZFBJDWTYEYAR8FZH5WKDTFJ9AHA.stx-token` 70 | 71 | before(async () => { 72 | ProviderRegistry.registerProvider( 73 | providerWithInitialAllocations(balances) 74 | ) 75 | provider = await ProviderRegistry.createProvider() 76 | 77 | src20TraitClient = new Client("ST3J2GVMMM2R07ZFBJDWTYEYAR8FZH5WKDTFJ9AHA.src20-trait", "src20-trait", provider) 78 | swaprTraitClient = new Client("ST3J2GVMMM2R07ZFBJDWTYEYAR8FZH5WKDTFJ9AHA.swapr-trait", "swapr-trait", provider) 79 | 80 | flexrClient = new FlexrClient("ST3J2GVMMM2R07ZFBJDWTYEYAR8FZH5WKDTFJ9AHA", provider) 81 | geyserClient = new GeyserClient("ST3J2GVMMM2R07ZFBJDWTYEYAR8FZH5WKDTFJ9AHA", provider) 82 | oracleClient = new OracleClient("ST3J2GVMMM2R07ZFBJDWTYEYAR8FZH5WKDTFJ9AHA", provider) 83 | swaprClient = new SwaprClient("ST3J2GVMMM2R07ZFBJDWTYEYAR8FZH5WKDTFJ9AHA", provider) 84 | flexrStxTokenClient = new FlexrStxTokenClient("flexr-stx", "ST3J2GVMMM2R07ZFBJDWTYEYAR8FZH5WKDTFJ9AHA", provider) 85 | stxClient = new StxClient("ST3J2GVMMM2R07ZFBJDWTYEYAR8FZH5WKDTFJ9AHA", provider) 86 | plaidClient = new PlaidClient("ST3J2GVMMM2R07ZFBJDWTYEYAR8FZH5WKDTFJ9AHA", provider) 87 | plaidStxTokenClient = new PlaidStxTokenClient("plaid-stx", "ST3J2GVMMM2R07ZFBJDWTYEYAR8FZH5WKDTFJ9AHA", provider) 88 | }) 89 | 90 | describe("Check contracts", () => { 91 | it("should have a valid syntax", async () => { 92 | await src20TraitClient.checkContract() 93 | await src20TraitClient.deployContract() 94 | 95 | await swaprTraitClient.checkContract() 96 | await swaprTraitClient.deployContract() 97 | 98 | await plaidClient.checkContract() 99 | await plaidClient.deployContract() 100 | 101 | await swaprClient.checkContract() 102 | await swaprClient.deployContract() 103 | 104 | await stxClient.checkContract() 105 | await stxClient.deployContract() 106 | 107 | await oracleClient.checkContract() 108 | await oracleClient.deployContract() 109 | 110 | await flexrClient.checkContract() 111 | await flexrClient.deployContract() 112 | 113 | await flexrStxTokenClient.checkContract() 114 | await flexrStxTokenClient.deployContract() 115 | 116 | await geyserClient.checkContract() 117 | await geyserClient.deployContract() 118 | 119 | await plaidStxTokenClient.checkContract() 120 | await plaidStxTokenClient.deployContract() 121 | }) 122 | }) 123 | 124 | describe("Full scenario", () => { 125 | before(async () => { 126 | // // wrap stx into wrapr 127 | // console.log("======> wrap.treasury") 128 | // assert(await wraprClient.wrap(50_000_000_000_000, {sender: flexr_treasury})) 129 | 130 | // create flerx-swapr pair 131 | console.log("======> createPair.treasury") 132 | assert(await swaprClient.createPair(flexr_token, stx_token, flexr_stx_token, "flexr-stx", 50_000_000_000_000, 50_000_000_000_000, {sender: flexr_treasury}), "createPair did not return true") 133 | 134 | 135 | // // Alice wraps STX 136 | // console.log("======> wrap.alice") 137 | // assert(await wraprClient.wrap(100_000_000_000, {sender: alice})) 138 | // Alice gets some FLEXR 139 | console.log("======> swapExactYforX.alice") 140 | assert(await swaprClient.swapYforExactX(flexr_token, stx_token, 40_000_000_000, {sender: alice})) 141 | // Alice add a position on swapr's flexr-stx pair 142 | console.log("======> addToPosition.alice") 143 | assert(await swaprClient.addToPosition(flexr_token, stx_token, flexr_stx_token, 40_000_000_000, 40_000_000_000, {sender: alice}), "addToPosition did not return true") 144 | // Alice stakes her position on geyser 145 | console.log("======> stake.alice") 146 | assert.equal(await flexrStxTokenClient.balanceOf(alice, {sender: alice}), 40_000_000_000) 147 | assert(await geyserClient.stake(40_000_000_000, {sender: alice}), "stake did not return true") 148 | assert.equal(await flexrClient.balanceOf(alice, {sender: alice}), 0) 149 | assert.equal(await flexrStxTokenClient.balanceOf(alice, {sender: alice}), 0) 150 | 151 | // // Bob wraps STX 152 | // console.log("======> wrap.bob") 153 | // assert(await wraprClient.wrap(50_000_000_000, {sender: bob})) 154 | 155 | // // Zoe wraps STX 156 | // console.log("======> wrap.zoe") 157 | // assert(await wraprClient.wrap(50_000_000_000, {sender: zoe})) 158 | // Zoe gets a lot of FLEXR 159 | console.log("======> swapExactYforX.zoe") 160 | assert(await swaprClient.swapExactYforX(flexr_token, stx_token, 50_000_000_000, {sender: zoe})) 161 | 162 | for (let i = 0; i < 5; i++) { 163 | console.log(`======> swapExactYforX.bob - round ${i}`) 164 | 165 | // Bob exhanges stx for flexr (back and forth 5x) 166 | console.log("======> swapExactYforX.bob") 167 | assert(await swaprClient.swapExactYforX(flexr_token, stx_token, 2_000_000_000, {sender: bob})) 168 | // Zoe exhanges flexr for stx (back and forth 5x) 169 | console.log("======> swapExactXforY.zoe") 170 | assert(await swaprClient.swapExactXforY(flexr_token, stx_token, 2_000_000_000, {sender: zoe})) 171 | 172 | console.log("======> updatePrice.zoe") 173 | assert(await oracleClient.updatePrice(prices[i], {sender: zoe})) 174 | console.log(`======> rebase.zoe - ${prices[i]}`) 175 | assert(await flexrClient.rebase({sender: zoe})) 176 | } 177 | 178 | // Alice collects her reward on geyser 179 | console.log("======> unstake.alice") 180 | assert(await geyserClient.unstake({sender: alice}), "stake did not return true") 181 | }) 182 | 183 | it("check balances after running scenario", async () => { 184 | // Alice checks the fees she collected 185 | assert.equal(await flexrClient.balanceOf(alice, {sender: alice}), 880_000) 186 | assert.equal(await flexrStxTokenClient.balanceOf(alice, {sender: alice}), 40_000_000_000) 187 | 188 | // total FLEXR supply 189 | assert.equal(await flexrClient.totalSupply({sender: alice}), 1_014_873_127_537_500) // starting value: 1_000_000_000_000_000 190 | }) 191 | 192 | }) 193 | 194 | after(async () => { 195 | await provider.close() 196 | }) 197 | }) 198 | -------------------------------------------------------------------------------- /test/unit/providerWithInitialAllocations.ts: -------------------------------------------------------------------------------- 1 | import * as os from 'os' 2 | import * as path from 'path' 3 | 4 | import { NativeClarityBinProvider } from '@blockstack/clarity' 5 | import { ProviderConstructor } from '@blockstack/clarity/lib/core/provider' 6 | import { InitialAllocation } from '@blockstack/clarity/lib/providers/clarityBin' 7 | import { getDefaultBinaryFilePath } from '@blockstack/clarity-native-bin' 8 | 9 | export function getTempFilePath(fileNameTemplate = 'temp-{uniqueID}-file') { 10 | const uniqueID = `${(Date.now() / 1000) | 0}-${Math.random().toString(36).substr(2, 6)}` 11 | const fileName = fileNameTemplate.replace('{uniqueID}', uniqueID) 12 | return path.join(os.tmpdir(), fileName) 13 | } 14 | 15 | export function providerWithInitialAllocations(allocations: InitialAllocation[]): ProviderConstructor { 16 | const nativeBinFile = getDefaultBinaryFilePath() 17 | const tempDbPath = getTempFilePath('blockstack-local-{uniqueID}.db') 18 | 19 | const providerConstructor: ProviderConstructor = { 20 | create: () => 21 | NativeClarityBinProvider.create(allocations, tempDbPath, nativeBinFile), 22 | } 23 | return providerConstructor 24 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "compilerOptions": { 4 | "target": "es2019", 5 | "module": "commonjs", 6 | "moduleResolution": "node", 7 | "declaration": false, 8 | "strict": true, 9 | "outDir": "lib", 10 | "sourceMap": false, 11 | "esModuleInterop": false, 12 | "baseUrl": ".", 13 | "noImplicitAny": false, 14 | "checkJs": false 15 | }, 16 | "include": [ 17 | "src", 18 | "tests" 19 | ], 20 | "exclude": [ 21 | "node_modules", 22 | "lib" 23 | ] 24 | } 25 | --------------------------------------------------------------------------------