diff --git a/contracts/BondCurveStep.aes b/contracts/BondCurveStep.aes new file mode 100644 index 0000000..78d7fda --- /dev/null +++ b/contracts/BondCurveStep.aes @@ -0,0 +1,108 @@ +@compiler >=4.2 + +include "Frac.aes" + +contract BondCurveStep = + + /** + * Alpha here is used to define the step of the curve + * and will determine the ratios for + * the reserve and the spread + * + * Adjust the second argument of Frac.make_frac() + * according to your fungible token decimals + * + * Example: + * - Frac.make_frac(1, 1) would mean we are working with + * the lowest possible integers. + * - Frac.make_frac(1, 1000000000000000000) - this config + * would be suitable for working with fungible tokens + * with 18 decimals. + */ + entrypoint alpha() : Frac.frac = Frac.make_frac(1, 1) + + // Initial price for the buy curve + entrypoint init_buy_price() : Frac.frac = Frac.from_int(1) + + // Initial price for the sell curve + entrypoint init_sell_price() : Frac.frac = Frac.from_int(0) + + // Returns the current buy price + entrypoint + buy_price : int => int + buy_price(0) = Frac.round(init_buy_price()) + buy_price(total_supply: int) : int = + Frac.ceil(buy_curve(Frac.from_int(total_supply))) + + // Returns the current sell price + entrypoint + sell_price : int => int + sell_price(0) = Frac.round(init_sell_price()) + sell_price(total_supply: int) : int = + Frac.ceil(sell_curve(Frac.from_int(total_supply))) + + /** + * Calculate the AE price for amount of tokens to be bought + * based on the total supply of the corresponding FT. + * + * Simplified equation to calculate the buy_price + * given the current token supply (total_supply) and desired amount of + * tokens to be acquired (buy_tokens): + * (buy_price() + buy_curve(total_supply + buy_tokens)) * buy_tokens / 2 + */ + entrypoint + calculate_buy_price : (int, int) => int + calculate_buy_price(total_supply: int, buy_tokens: int) : int = + require(total_supply >= 0, "ERROR_TOTAL_SUPPLY_NEGATIVE") + require(buy_tokens >= 0, "ERROR_BUY_TOKENS_NEGATIVE") + let buy_tokens_amount = Frac.from_int(buy_tokens) + let total_supply_frac = Frac.from_int(total_supply) + Frac.ceil(Frac.div( + Frac.mul( + (Frac.add( Frac.from_int(buy_price(total_supply)) + , buy_curve(Frac.add( total_supply_frac + , buy_tokens_amount)))) + , buy_tokens_amount + ) , Frac.from_int(2))) + + /** + * Returns the amount of AE tokens to be received + * when selling fungible tokens back to the curve. + * + * Simplified equation to calculate the return amount + * given the current token supply (total_supply) and desired amount of + * tokens to be sold back to the curve (sell_tokens): + * (sell_price() + sell_curve(total_supply - sell_tokens)) * sell_tokens / 2 + */ + entrypoint + calculate_sell_return : (int, int) => int + calculate_sell_return(total_supply: int, sell_tokens: int) : int = + require(total_supply >= 0, "ERROR_TOTAL_SUPPLY_NEGATIVE") + require(sell_tokens >= 0, "ERROR_SELL_TOKENS_NEGATIVE") + require(total_supply >= sell_tokens, "ERROR_SELL_INSUFFICIENT_TOTAL_SUPPLY") + let sell_tokens_amount = Frac.from_int(sell_tokens) + let total_supply_frac = Frac.from_int(total_supply) + Frac.ceil(Frac.div( + Frac.mul( + (Frac.add( Frac.from_int(sell_price(total_supply)) + , sell_curve(Frac.sub( total_supply_frac + , sell_tokens_amount)))) + , sell_tokens_amount + ) , Frac.from_int(2))) + + // Step Buy curve formula + entrypoint buy_curve(x: Frac.frac) : Frac.frac = + require(Frac.is_sane(x), "INVALID_FRAC_NUMBER") + Frac.from_int(Frac.ceil(Frac.mul(x, alpha()))) + + // Step Sell curve formula + entrypoint sell_curve(x: Frac.frac) : Frac.frac = + require(Frac.is_sane(x), "INVALID_FRAC_NUMBER") + Frac.sub( + Frac.from_int( + Frac.floor( + Frac.mul( x + , alpha() ) + ) + ) + , Frac.from_int(1)) \ No newline at end of file diff --git a/contracts/BondCurveStepInterface.aes b/contracts/BondCurveStepInterface.aes new file mode 100644 index 0000000..43c0159 --- /dev/null +++ b/contracts/BondCurveStepInterface.aes @@ -0,0 +1,19 @@ +@compiler >=4.2 + +/** + * Step Bonding Curve Interface + */ +contract BondCurveStepInterface = + // Returns the current buy price + entrypoint buy_price : (int) => int + + // Returns the current sell price + entrypoint sell_price : (int) => int + + // Returns the AE price for the desired amount + // of tokens to be bought from the curve. + entrypoint calculate_buy_price : (int, int) => int + + // Returns the amount of AE tokens to be received back + // when selling fungible tokens back to the curve. + entrypoint calculate_sell_return : (int, int) => int \ No newline at end of file diff --git a/test/bondCurveLinearTest.js b/test/bondCurveLinearTest.js index 427b099..d0e6e44 100644 --- a/test/bondCurveLinearTest.js +++ b/test/bondCurveLinearTest.js @@ -58,7 +58,7 @@ describe('Bonding Curve Contract', () => { describe('Buy current price tests', () => { it.each( - [...testData], + [...testData.linear], 'Should get buy price for supply %s', ['element'], (p, next) => { @@ -76,7 +76,7 @@ describe('Bonding Curve Contract', () => { describe('Sell current price tests', () => { it.each( - [...testData], + [...testData.linear], 'Should get sell price for supply %s', ['element'], (p, next) => { @@ -94,7 +94,7 @@ describe('Bonding Curve Contract', () => { describe('Calculate Buy price tests', () => { it.each( - [...testData], + [...testData.linear], 'Should calculate buy price for supply %s', ['element'], (p, next) => { @@ -114,7 +114,7 @@ describe('Bonding Curve Contract', () => { describe('Sell price tests', () => { it.each( - [...testData], + [...testData.linear], 'Should calculate sell return for supply %s', ['element'], (p, next) => { diff --git a/test/bondCurveStepTest.js b/test/bondCurveStepTest.js new file mode 100644 index 0000000..0d510d6 --- /dev/null +++ b/test/bondCurveStepTest.js @@ -0,0 +1,160 @@ +/* + * ISC License (ISC) + * Copyright (c) 2018 aeternity developers + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH + * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, + * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM + * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR + * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + * PERFORMANCE OF THIS SOFTWARE. + */ + +require('it-each')({ testPerIteration: true }); +const { Universal, MemoryAccount, Node } = require('@aeternity/aepp-sdk'); +const BONDING_CURVE_STEP_CONTRACT = utils.readFileRelative( + './contracts/BondCurveStep.aes', + 'utf-8', +); +const testData = require('./data'); + +const config = { + url: 'http://localhost:3001/', + internalUrl: 'http://localhost:3001/', + compilerUrl: 'http://localhost:3080', +}; + +describe('Bonding Curve Step Contract', () => { + let client, contract; + + before(async () => { + client = await Universal({ + nodes: [ + { + name: 'devnetNode', + instance: await Node(config), + }, + ], + accounts: [ + MemoryAccount({ + keypair: wallets[0], + }), + ], + networkId: 'ae_devnet', + compilerUrl: config.compilerUrl, + }); + }); + + it('Deploying Bond Contract', async () => { + contract = await client.getContractInstance(BONDING_CURVE_STEP_CONTRACT); + const init = await contract.methods.init(); + assert.equal(init.result.returnType, 'ok'); + }); + + // describe('Buy current price tests', () => { + // it.each( + // [...testData.step], + // 'Should get buy price for supply', + // ['element'], + // (p, next) => { + // contract.methods.buy_price(p.totalSupply).then((result) => { + // assert.equal( + // result.decodedResult, + // p.totalSupply + 1, + // `Buy price incorrect for supply: ${JSON.stringify(p)}`, + // ); + // next(); + // }); + // }, + // ); + // }); + + // describe('Sell current price tests', () => { + // it.each( + // [...testData.step], + // 'Should get sell price for supply', + // ['element'], + // (p, next) => { + // contract.methods.sell_price(p.totalSupply).then((result) => { + // assert.equal( + // result.decodedResult, + // p.totalSupply, + // `Sell price incorrect for supply: ${JSON.stringify(p)}`, + // ); + // next(); + // }); + // }, + // ); + // }); + + describe('Calculate Buy price tests', () => { + it.each( + [...testData.step], + 'Should calculate buy price for supply', + ['element'], + (p, next) => { + contract.methods + .calculate_buy_price(p.totalSupply, p.buy.amount) + .then((result) => { + assert.equal( + result.decodedResult, + p.buy.aettos, + `Calculation for buy price incorrect for: ${JSON.stringify(p)}`, + ); + next(); + }); + }, + ); + }); + + describe('Sell price tests', () => { + it.each( + [...testData.step], + 'Should calculate sell return for supply', + ['element'], + (p, next) => { + if (p.totalSupply >= p.sell.amount) { + contract.methods + .calculate_sell_return(p.totalSupply, p.sell.amount) + .then((result) => { + assert.equal( + result.decodedResult, + p.sell.aettos, + `Calculation for sell price incorrect for: ${JSON.stringify( + p, + )}`, + ); + next(); + }); + } else { + contract.methods + .calculate_sell_return(p.totalSupply, p.sell.amount) + .then((result) => { + assert.equal( + result.decodedResult, + p.sell.aettos, + `Calculation for sell price incorrect for: ${JSON.stringify( + p, + )}`, + ); + next(); + }) + .catch((e) => { + if ( + e.decodedError.indexOf( + 'ERROR_SELL_INSUFFICIENT_TOTAL_SUPPLY' > -1, + ) + ) { + next(); + } + }); + } + }, + ); + }); +}); diff --git a/test/data.js b/test/data.js index bf0e5a5..2655007 100644 --- a/test/data.js +++ b/test/data.js @@ -1,62 +1,131 @@ -module.exports = [ - { - totalSupply: 0, - buy: { amount: 1123, aettos: 631688 }, - sell: { amount: 1, aettos: 0 }, - }, - { - totalSupply: 2, - buy: { amount: 44234, aettos: 978456080 }, - sell: { amount: 11231, aettos: 0 }, - }, - { - totalSupply: 3, - buy: { amount: 118723, aettos: 7048050257 }, - sell: { amount: 15523, aettos: 0 }, - }, - { - totalSupply: 7, - buy: { amount: 747239841, aettos: '279183695966771369' }, - sell: { amount: 3123123, aettos: 0 }, - }, - { - totalSupply: 42, - buy: { amount: 2348231, aettos: 2757195388614 }, - sell: { amount: 234923, aettos: 0 }, - }, - { - totalSupply: 144, - buy: { amount: 2349241, aettos: 2759807277986 }, - sell: { amount: 2349921, aettos: 0 }, - }, - { - totalSupply: 256, - buy: { amount: 5437811, aettos: 14786291753288 }, - sell: { amount: 42394, aettos: 0 }, - }, - { - totalSupply: 1237, - buy: { amount: 2349021, aettos: 2761857917219 }, - sell: { amount: 1283712, aettos: 0 }, - }, - { - totalSupply: 88321, - buy: { amount: 32095831, aettos: 517905951775863 }, - sell: { amount: 13882, aettos: 1129717160 }, - }, - { - totalSupply: 9238123, - buy: { amount: 1238171, aettos: 12204910943825 }, - sell: { amount: 142349, aettos: 1304905952027 }, - }, - { - totalSupply: 2138123173912, - buy: { amount: 2349241, aettos: '5022969382673188074' }, - sell: { amount: 49214234, aettos: '105224883181313760030' }, - }, - { - totalSupply: 123987123123817, - buy: { amount: 1223492, aettos: '151697253993472669488' }, - sell: { amount: 2349293492, aettos: '291279381856630211521932' }, - }, -]; +module.exports = { + linear: [ + { + totalSupply: 0, + buy: { amount: 1123, aettos: 631688 }, + sell: { amount: 1, aettos: 0 }, + }, + { + totalSupply: 2, + buy: { amount: 44234, aettos: 978456080 }, + sell: { amount: 11231, aettos: 0 }, + }, + { + totalSupply: 3, + buy: { amount: 118723, aettos: 7048050257 }, + sell: { amount: 15523, aettos: 0 }, + }, + { + totalSupply: 7, + buy: { amount: 747239841, aettos: '279183695966771369' }, + sell: { amount: 3123123, aettos: 0 }, + }, + { + totalSupply: 42, + buy: { amount: 2348231, aettos: 2757195388614 }, + sell: { amount: 234923, aettos: 0 }, + }, + { + totalSupply: 144, + buy: { amount: 2349241, aettos: 2759807277986 }, + sell: { amount: 2349921, aettos: 0 }, + }, + { + totalSupply: 256, + buy: { amount: 5437811, aettos: 14786291753288 }, + sell: { amount: 42394, aettos: 0 }, + }, + { + totalSupply: 1237, + buy: { amount: 2349021, aettos: 2761857917219 }, + sell: { amount: 1283712, aettos: 0 }, + }, + { + totalSupply: 88321, + buy: { amount: 32095831, aettos: 517905951775863 }, + sell: { amount: 13882, aettos: 1129717160 }, + }, + { + totalSupply: 9238123, + buy: { amount: 1238171, aettos: 12204910943825 }, + sell: { amount: 142349, aettos: 1304905952027 }, + }, + { + totalSupply: 2138123173912, + buy: { amount: 2349241, aettos: '5022969382673188074' }, + sell: { amount: 49214234, aettos: '105224883181313760030' }, + }, + { + totalSupply: 123987123123817, + buy: { amount: 1223492, aettos: '151697253993472669488' }, + sell: { amount: 2349293492, aettos: '291279381856630211521932' }, + }, + ], + step: [ + { + totalSupply: 0, + buy: { amount: 1, aettos: 1 }, + sell: { amount: 1, aettos: 0 }, + }, + { + totalSupply: 0, + buy: { amount: 1123, aettos: 631126 }, + sell: { amount: 1, aettos: 0 }, + }, + { + totalSupply: 2, + buy: { amount: 44234, aettos: 978411846 }, + sell: { amount: 11231, aettos: 0 }, + }, + { + totalSupply: 3, + buy: { amount: 118723, aettos: 7047931534 }, + sell: { amount: 15523, aettos: 0 }, + }, + { + totalSupply: 7, + buy: { amount: 747239841, aettos: '279183695219531528' }, + sell: { amount: 3123123, aettos: 0 }, + }, + { + totalSupply: 42, + buy: { amount: 2348231, aettos: 2757193040383 }, + sell: { amount: 234923, aettos: 0 }, + }, + { + totalSupply: 144, + buy: { amount: 2349241, aettos: 2759804928745 }, + sell: { amount: 2349921, aettos: 0 }, + }, + { + totalSupply: 256, + buy: { amount: 5437811, aettos: 14786286315477 }, + sell: { amount: 42394, aettos: 0 }, + }, + { + totalSupply: 1237, + buy: { amount: 2349021, aettos: 2761855568198 }, + sell: { amount: 1283712, aettos: 0 }, + }, + { + totalSupply: 88321, + buy: { amount: 32095831, aettos: 517905919680032 }, + sell: { amount: 13882, aettos: 1129717160 }, + }, + { + totalSupply: 9238123, + buy: { amount: 1238171, aettos: 12204909705654 }, + sell: { amount: 142349, aettos: 1304905952027 }, + }, + { + totalSupply: 2138123173912, + buy: { amount: 2349241, aettos: '5022969382670838833' }, + sell: { amount: 49214234, aettos: '105224883181313760030' }, + }, + { + totalSupply: 123987123123817, + buy: { amount: 1223492, aettos: '151697253993471445996' }, + sell: { amount: 2349293492, aettos: '291279381856630211521932' }, + }, + ], +};