ERC4626.test.js 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856
  1. const { constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers');
  2. const { expect } = require('chai');
  3. const ERC20Decimals = artifacts.require('$ERC20DecimalsMock');
  4. const ERC4626 = artifacts.require('$ERC4626');
  5. const ERC4626OffsetMock = artifacts.require('$ERC4626OffsetMock');
  6. const ERC4626FeesMock = artifacts.require('$ERC4626FeesMock');
  7. const ERC20ExcessDecimalsMock = artifacts.require('ERC20ExcessDecimalsMock');
  8. contract('ERC4626', function (accounts) {
  9. const [holder, recipient, spender, other, user1, user2] = accounts;
  10. const name = 'My Token';
  11. const symbol = 'MTKN';
  12. const decimals = web3.utils.toBN(18);
  13. it('inherit decimals if from asset', async function () {
  14. for (const decimals of [0, 9, 12, 18, 36].map(web3.utils.toBN)) {
  15. const token = await ERC20Decimals.new('', '', decimals);
  16. const vault = await ERC4626.new('', '', token.address);
  17. expect(await vault.decimals()).to.be.bignumber.equal(decimals);
  18. }
  19. });
  20. it('asset has not yet been created', async function () {
  21. const vault = await ERC4626.new('', '', other);
  22. expect(await vault.decimals()).to.be.bignumber.equal(decimals);
  23. });
  24. it('underlying excess decimals', async function () {
  25. const token = await ERC20ExcessDecimalsMock.new();
  26. const vault = await ERC4626.new('', '', token.address);
  27. expect(await vault.decimals()).to.be.bignumber.equal(decimals);
  28. });
  29. it('decimals overflow', async function () {
  30. for (const offset of [243, 250, 255].map(web3.utils.toBN)) {
  31. const token = await ERC20Decimals.new('', '', decimals);
  32. const vault = await ERC4626OffsetMock.new(name + ' Vault', symbol + 'V', token.address, offset);
  33. await expectRevert(
  34. vault.decimals(),
  35. 'reverted with panic code 0x11 (Arithmetic operation underflowed or overflowed outside of an unchecked block)',
  36. );
  37. }
  38. });
  39. for (const offset of [0, 6, 18].map(web3.utils.toBN)) {
  40. const parseToken = token => web3.utils.toBN(10).pow(decimals).muln(token);
  41. const parseShare = share => web3.utils.toBN(10).pow(decimals.add(offset)).muln(share);
  42. const virtualAssets = web3.utils.toBN(1);
  43. const virtualShares = web3.utils.toBN(10).pow(offset);
  44. describe(`offset: ${offset}`, function () {
  45. beforeEach(async function () {
  46. this.token = await ERC20Decimals.new(name, symbol, decimals);
  47. this.vault = await ERC4626OffsetMock.new(name + ' Vault', symbol + 'V', this.token.address, offset);
  48. await this.token.$_mint(holder, constants.MAX_INT256); // 50% of maximum
  49. await this.token.approve(this.vault.address, constants.MAX_UINT256, { from: holder });
  50. await this.vault.approve(spender, constants.MAX_UINT256, { from: holder });
  51. });
  52. it('metadata', async function () {
  53. expect(await this.vault.name()).to.be.equal(name + ' Vault');
  54. expect(await this.vault.symbol()).to.be.equal(symbol + 'V');
  55. expect(await this.vault.decimals()).to.be.bignumber.equal(decimals.add(offset));
  56. expect(await this.vault.asset()).to.be.equal(this.token.address);
  57. });
  58. describe('empty vault: no assets & no shares', function () {
  59. it('status', async function () {
  60. expect(await this.vault.totalAssets()).to.be.bignumber.equal('0');
  61. });
  62. it('deposit', async function () {
  63. expect(await this.vault.maxDeposit(holder)).to.be.bignumber.equal(constants.MAX_UINT256);
  64. expect(await this.vault.previewDeposit(parseToken(1))).to.be.bignumber.equal(parseShare(1));
  65. const { tx } = await this.vault.deposit(parseToken(1), recipient, { from: holder });
  66. await expectEvent.inTransaction(tx, this.token, 'Transfer', {
  67. from: holder,
  68. to: this.vault.address,
  69. value: parseToken(1),
  70. });
  71. await expectEvent.inTransaction(tx, this.vault, 'Transfer', {
  72. from: constants.ZERO_ADDRESS,
  73. to: recipient,
  74. value: parseShare(1),
  75. });
  76. await expectEvent.inTransaction(tx, this.vault, 'Deposit', {
  77. sender: holder,
  78. owner: recipient,
  79. assets: parseToken(1),
  80. shares: parseShare(1),
  81. });
  82. });
  83. it('mint', async function () {
  84. expect(await this.vault.maxMint(holder)).to.be.bignumber.equal(constants.MAX_UINT256);
  85. expect(await this.vault.previewMint(parseShare(1))).to.be.bignumber.equal(parseToken(1));
  86. const { tx } = await this.vault.mint(parseShare(1), recipient, { from: holder });
  87. await expectEvent.inTransaction(tx, this.token, 'Transfer', {
  88. from: holder,
  89. to: this.vault.address,
  90. value: parseToken(1),
  91. });
  92. await expectEvent.inTransaction(tx, this.vault, 'Transfer', {
  93. from: constants.ZERO_ADDRESS,
  94. to: recipient,
  95. value: parseShare(1),
  96. });
  97. await expectEvent.inTransaction(tx, this.vault, 'Deposit', {
  98. sender: holder,
  99. owner: recipient,
  100. assets: parseToken(1),
  101. shares: parseShare(1),
  102. });
  103. });
  104. it('withdraw', async function () {
  105. expect(await this.vault.maxWithdraw(holder)).to.be.bignumber.equal('0');
  106. expect(await this.vault.previewWithdraw('0')).to.be.bignumber.equal('0');
  107. const { tx } = await this.vault.withdraw('0', recipient, holder, { from: holder });
  108. await expectEvent.inTransaction(tx, this.token, 'Transfer', {
  109. from: this.vault.address,
  110. to: recipient,
  111. value: '0',
  112. });
  113. await expectEvent.inTransaction(tx, this.vault, 'Transfer', {
  114. from: holder,
  115. to: constants.ZERO_ADDRESS,
  116. value: '0',
  117. });
  118. await expectEvent.inTransaction(tx, this.vault, 'Withdraw', {
  119. sender: holder,
  120. receiver: recipient,
  121. owner: holder,
  122. assets: '0',
  123. shares: '0',
  124. });
  125. });
  126. it('redeem', async function () {
  127. expect(await this.vault.maxRedeem(holder)).to.be.bignumber.equal('0');
  128. expect(await this.vault.previewRedeem('0')).to.be.bignumber.equal('0');
  129. const { tx } = await this.vault.redeem('0', recipient, holder, { from: holder });
  130. await expectEvent.inTransaction(tx, this.token, 'Transfer', {
  131. from: this.vault.address,
  132. to: recipient,
  133. value: '0',
  134. });
  135. await expectEvent.inTransaction(tx, this.vault, 'Transfer', {
  136. from: holder,
  137. to: constants.ZERO_ADDRESS,
  138. value: '0',
  139. });
  140. await expectEvent.inTransaction(tx, this.vault, 'Withdraw', {
  141. sender: holder,
  142. receiver: recipient,
  143. owner: holder,
  144. assets: '0',
  145. shares: '0',
  146. });
  147. });
  148. });
  149. describe('inflation attack: offset price by direct deposit of assets', function () {
  150. beforeEach(async function () {
  151. // Donate 1 token to the vault to offset the price
  152. await this.token.$_mint(this.vault.address, parseToken(1));
  153. });
  154. it('status', async function () {
  155. expect(await this.vault.totalSupply()).to.be.bignumber.equal('0');
  156. expect(await this.vault.totalAssets()).to.be.bignumber.equal(parseToken(1));
  157. });
  158. /**
  159. * | offset | deposited assets | redeemable assets |
  160. * |--------|----------------------|----------------------|
  161. * | 0 | 1.000000000000000000 | 0. |
  162. * | 6 | 1.000000000000000000 | 0.999999000000000000 |
  163. * | 18 | 1.000000000000000000 | 0.999999999999999999 |
  164. *
  165. * Attack is possible, but made difficult by the offset. For the attack to be successful
  166. * the attacker needs to frontrun a deposit 10**offset times bigger than what the victim
  167. * was trying to deposit
  168. */
  169. it('deposit', async function () {
  170. const effectiveAssets = await this.vault.totalAssets().then(x => x.add(virtualAssets));
  171. const effectiveShares = await this.vault.totalSupply().then(x => x.add(virtualShares));
  172. const depositAssets = parseToken(1);
  173. const expectedShares = depositAssets.mul(effectiveShares).div(effectiveAssets);
  174. expect(await this.vault.maxDeposit(holder)).to.be.bignumber.equal(constants.MAX_UINT256);
  175. expect(await this.vault.previewDeposit(depositAssets)).to.be.bignumber.equal(expectedShares);
  176. const { tx } = await this.vault.deposit(depositAssets, recipient, { from: holder });
  177. await expectEvent.inTransaction(tx, this.token, 'Transfer', {
  178. from: holder,
  179. to: this.vault.address,
  180. value: depositAssets,
  181. });
  182. await expectEvent.inTransaction(tx, this.vault, 'Transfer', {
  183. from: constants.ZERO_ADDRESS,
  184. to: recipient,
  185. value: expectedShares,
  186. });
  187. await expectEvent.inTransaction(tx, this.vault, 'Deposit', {
  188. sender: holder,
  189. owner: recipient,
  190. assets: depositAssets,
  191. shares: expectedShares,
  192. });
  193. });
  194. /**
  195. * | offset | deposited assets | redeemable assets |
  196. * |--------|----------------------|----------------------|
  197. * | 0 | 1000000000000000001. | 1000000000000000001. |
  198. * | 6 | 1000000000000000001. | 1000000000000000001. |
  199. * | 18 | 1000000000000000001. | 1000000000000000001. |
  200. *
  201. * Using mint protects against inflation attack, but makes minting shares very expensive.
  202. * The ER20 allowance for the underlying asset is needed to protect the user from (too)
  203. * large deposits.
  204. */
  205. it('mint', async function () {
  206. const effectiveAssets = await this.vault.totalAssets().then(x => x.add(virtualAssets));
  207. const effectiveShares = await this.vault.totalSupply().then(x => x.add(virtualShares));
  208. const mintShares = parseShare(1);
  209. const expectedAssets = mintShares.mul(effectiveAssets).div(effectiveShares);
  210. expect(await this.vault.maxMint(holder)).to.be.bignumber.equal(constants.MAX_UINT256);
  211. expect(await this.vault.previewMint(mintShares)).to.be.bignumber.equal(expectedAssets);
  212. const { tx } = await this.vault.mint(mintShares, recipient, { from: holder });
  213. await expectEvent.inTransaction(tx, this.token, 'Transfer', {
  214. from: holder,
  215. to: this.vault.address,
  216. value: expectedAssets,
  217. });
  218. await expectEvent.inTransaction(tx, this.vault, 'Transfer', {
  219. from: constants.ZERO_ADDRESS,
  220. to: recipient,
  221. value: mintShares,
  222. });
  223. await expectEvent.inTransaction(tx, this.vault, 'Deposit', {
  224. sender: holder,
  225. owner: recipient,
  226. assets: expectedAssets,
  227. shares: mintShares,
  228. });
  229. });
  230. it('withdraw', async function () {
  231. expect(await this.vault.maxWithdraw(holder)).to.be.bignumber.equal('0');
  232. expect(await this.vault.previewWithdraw('0')).to.be.bignumber.equal('0');
  233. const { tx } = await this.vault.withdraw('0', recipient, holder, { from: holder });
  234. await expectEvent.inTransaction(tx, this.token, 'Transfer', {
  235. from: this.vault.address,
  236. to: recipient,
  237. value: '0',
  238. });
  239. await expectEvent.inTransaction(tx, this.vault, 'Transfer', {
  240. from: holder,
  241. to: constants.ZERO_ADDRESS,
  242. value: '0',
  243. });
  244. await expectEvent.inTransaction(tx, this.vault, 'Withdraw', {
  245. sender: holder,
  246. receiver: recipient,
  247. owner: holder,
  248. assets: '0',
  249. shares: '0',
  250. });
  251. });
  252. it('redeem', async function () {
  253. expect(await this.vault.maxRedeem(holder)).to.be.bignumber.equal('0');
  254. expect(await this.vault.previewRedeem('0')).to.be.bignumber.equal('0');
  255. const { tx } = await this.vault.redeem('0', recipient, holder, { from: holder });
  256. await expectEvent.inTransaction(tx, this.token, 'Transfer', {
  257. from: this.vault.address,
  258. to: recipient,
  259. value: '0',
  260. });
  261. await expectEvent.inTransaction(tx, this.vault, 'Transfer', {
  262. from: holder,
  263. to: constants.ZERO_ADDRESS,
  264. value: '0',
  265. });
  266. await expectEvent.inTransaction(tx, this.vault, 'Withdraw', {
  267. sender: holder,
  268. receiver: recipient,
  269. owner: holder,
  270. assets: '0',
  271. shares: '0',
  272. });
  273. });
  274. });
  275. describe('full vault: assets & shares', function () {
  276. beforeEach(async function () {
  277. // Add 1 token of underlying asset and 100 shares to the vault
  278. await this.token.$_mint(this.vault.address, parseToken(1));
  279. await this.vault.$_mint(holder, parseShare(100));
  280. });
  281. it('status', async function () {
  282. expect(await this.vault.totalSupply()).to.be.bignumber.equal(parseShare(100));
  283. expect(await this.vault.totalAssets()).to.be.bignumber.equal(parseToken(1));
  284. });
  285. /**
  286. * | offset | deposited assets | redeemable assets |
  287. * |--------|--------------------- |----------------------|
  288. * | 0 | 1.000000000000000000 | 0.999999999999999999 |
  289. * | 6 | 1.000000000000000000 | 0.999999999999999999 |
  290. * | 18 | 1.000000000000000000 | 0.999999999999999999 |
  291. *
  292. * Virtual shares & assets captures part of the value
  293. */
  294. it('deposit', async function () {
  295. const effectiveAssets = await this.vault.totalAssets().then(x => x.add(virtualAssets));
  296. const effectiveShares = await this.vault.totalSupply().then(x => x.add(virtualShares));
  297. const depositAssets = parseToken(1);
  298. const expectedShares = depositAssets.mul(effectiveShares).div(effectiveAssets);
  299. expect(await this.vault.maxDeposit(holder)).to.be.bignumber.equal(constants.MAX_UINT256);
  300. expect(await this.vault.previewDeposit(depositAssets)).to.be.bignumber.equal(expectedShares);
  301. const { tx } = await this.vault.deposit(depositAssets, recipient, { from: holder });
  302. await expectEvent.inTransaction(tx, this.token, 'Transfer', {
  303. from: holder,
  304. to: this.vault.address,
  305. value: depositAssets,
  306. });
  307. await expectEvent.inTransaction(tx, this.vault, 'Transfer', {
  308. from: constants.ZERO_ADDRESS,
  309. to: recipient,
  310. value: expectedShares,
  311. });
  312. await expectEvent.inTransaction(tx, this.vault, 'Deposit', {
  313. sender: holder,
  314. owner: recipient,
  315. assets: depositAssets,
  316. shares: expectedShares,
  317. });
  318. });
  319. /**
  320. * | offset | deposited assets | redeemable assets |
  321. * |--------|--------------------- |----------------------|
  322. * | 0 | 0.010000000000000001 | 0.010000000000000000 |
  323. * | 6 | 0.010000000000000001 | 0.010000000000000000 |
  324. * | 18 | 0.010000000000000001 | 0.010000000000000000 |
  325. *
  326. * Virtual shares & assets captures part of the value
  327. */
  328. it('mint', async function () {
  329. const effectiveAssets = await this.vault.totalAssets().then(x => x.add(virtualAssets));
  330. const effectiveShares = await this.vault.totalSupply().then(x => x.add(virtualShares));
  331. const mintShares = parseShare(1);
  332. const expectedAssets = mintShares.mul(effectiveAssets).div(effectiveShares).addn(1); // add for the rounding
  333. expect(await this.vault.maxMint(holder)).to.be.bignumber.equal(constants.MAX_UINT256);
  334. expect(await this.vault.previewMint(mintShares)).to.be.bignumber.equal(expectedAssets);
  335. const { tx } = await this.vault.mint(mintShares, recipient, { from: holder });
  336. await expectEvent.inTransaction(tx, this.token, 'Transfer', {
  337. from: holder,
  338. to: this.vault.address,
  339. value: expectedAssets,
  340. });
  341. await expectEvent.inTransaction(tx, this.vault, 'Transfer', {
  342. from: constants.ZERO_ADDRESS,
  343. to: recipient,
  344. value: mintShares,
  345. });
  346. await expectEvent.inTransaction(tx, this.vault, 'Deposit', {
  347. sender: holder,
  348. owner: recipient,
  349. assets: expectedAssets,
  350. shares: mintShares,
  351. });
  352. });
  353. it('withdraw', async function () {
  354. const effectiveAssets = await this.vault.totalAssets().then(x => x.add(virtualAssets));
  355. const effectiveShares = await this.vault.totalSupply().then(x => x.add(virtualShares));
  356. const withdrawAssets = parseToken(1);
  357. const expectedShares = withdrawAssets.mul(effectiveShares).div(effectiveAssets).addn(1); // add for the rounding
  358. expect(await this.vault.maxWithdraw(holder)).to.be.bignumber.equal(withdrawAssets);
  359. expect(await this.vault.previewWithdraw(withdrawAssets)).to.be.bignumber.equal(expectedShares);
  360. const { tx } = await this.vault.withdraw(withdrawAssets, recipient, holder, { from: holder });
  361. await expectEvent.inTransaction(tx, this.token, 'Transfer', {
  362. from: this.vault.address,
  363. to: recipient,
  364. value: withdrawAssets,
  365. });
  366. await expectEvent.inTransaction(tx, this.vault, 'Transfer', {
  367. from: holder,
  368. to: constants.ZERO_ADDRESS,
  369. value: expectedShares,
  370. });
  371. await expectEvent.inTransaction(tx, this.vault, 'Withdraw', {
  372. sender: holder,
  373. receiver: recipient,
  374. owner: holder,
  375. assets: withdrawAssets,
  376. shares: expectedShares,
  377. });
  378. });
  379. it('withdraw with approval', async function () {
  380. await expectRevert(
  381. this.vault.withdraw(parseToken(1), recipient, holder, { from: other }),
  382. 'ERC20: insufficient allowance',
  383. );
  384. await this.vault.withdraw(parseToken(1), recipient, holder, { from: spender });
  385. });
  386. it('redeem', async function () {
  387. const effectiveAssets = await this.vault.totalAssets().then(x => x.add(virtualAssets));
  388. const effectiveShares = await this.vault.totalSupply().then(x => x.add(virtualShares));
  389. const redeemShares = parseShare(100);
  390. const expectedAssets = redeemShares.mul(effectiveAssets).div(effectiveShares);
  391. expect(await this.vault.maxRedeem(holder)).to.be.bignumber.equal(redeemShares);
  392. expect(await this.vault.previewRedeem(redeemShares)).to.be.bignumber.equal(expectedAssets);
  393. const { tx } = await this.vault.redeem(redeemShares, recipient, holder, { from: holder });
  394. await expectEvent.inTransaction(tx, this.token, 'Transfer', {
  395. from: this.vault.address,
  396. to: recipient,
  397. value: expectedAssets,
  398. });
  399. await expectEvent.inTransaction(tx, this.vault, 'Transfer', {
  400. from: holder,
  401. to: constants.ZERO_ADDRESS,
  402. value: redeemShares,
  403. });
  404. await expectEvent.inTransaction(tx, this.vault, 'Withdraw', {
  405. sender: holder,
  406. receiver: recipient,
  407. owner: holder,
  408. assets: expectedAssets,
  409. shares: redeemShares,
  410. });
  411. });
  412. it('redeem with approval', async function () {
  413. await expectRevert(
  414. this.vault.redeem(parseShare(100), recipient, holder, { from: other }),
  415. 'ERC20: insufficient allowance',
  416. );
  417. await this.vault.redeem(parseShare(100), recipient, holder, { from: spender });
  418. });
  419. });
  420. });
  421. }
  422. describe('ERC4626Fees', function () {
  423. const feeBasePoint = web3.utils.toBN(5e3);
  424. const amountWithoutFees = web3.utils.toBN(10000);
  425. const fees = amountWithoutFees.mul(feeBasePoint).divn(1e5);
  426. const amountWithFees = amountWithoutFees.add(fees);
  427. describe('input fees', function () {
  428. beforeEach(async function () {
  429. this.token = await ERC20Decimals.new(name, symbol, 18);
  430. this.vault = await ERC4626FeesMock.new(
  431. name + ' Vault',
  432. symbol + 'V',
  433. this.token.address,
  434. feeBasePoint,
  435. other,
  436. 0,
  437. constants.ZERO_ADDRESS,
  438. );
  439. await this.token.$_mint(holder, constants.MAX_INT256);
  440. await this.token.approve(this.vault.address, constants.MAX_INT256, { from: holder });
  441. });
  442. it('deposit', async function () {
  443. expect(await this.vault.previewDeposit(amountWithFees)).to.be.bignumber.equal(amountWithoutFees);
  444. ({ tx: this.tx } = await this.vault.deposit(amountWithFees, recipient, { from: holder }));
  445. });
  446. it('mint', async function () {
  447. expect(await this.vault.previewMint(amountWithoutFees)).to.be.bignumber.equal(amountWithFees);
  448. ({ tx: this.tx } = await this.vault.mint(amountWithoutFees, recipient, { from: holder }));
  449. });
  450. afterEach(async function () {
  451. // get total
  452. await expectEvent.inTransaction(this.tx, this.token, 'Transfer', {
  453. from: holder,
  454. to: this.vault.address,
  455. value: amountWithFees,
  456. });
  457. // redirect fees
  458. await expectEvent.inTransaction(this.tx, this.token, 'Transfer', {
  459. from: this.vault.address,
  460. to: other,
  461. value: fees,
  462. });
  463. // mint shares
  464. await expectEvent.inTransaction(this.tx, this.vault, 'Transfer', {
  465. from: constants.ZERO_ADDRESS,
  466. to: recipient,
  467. value: amountWithoutFees,
  468. });
  469. // deposit event
  470. await expectEvent.inTransaction(this.tx, this.vault, 'Deposit', {
  471. sender: holder,
  472. owner: recipient,
  473. assets: amountWithFees,
  474. shares: amountWithoutFees,
  475. });
  476. });
  477. });
  478. describe('output fees', function () {
  479. beforeEach(async function () {
  480. this.token = await ERC20Decimals.new(name, symbol, 18);
  481. this.vault = await ERC4626FeesMock.new(
  482. name + ' Vault',
  483. symbol + 'V',
  484. this.token.address,
  485. 0,
  486. constants.ZERO_ADDRESS,
  487. 5e3, // 5%
  488. other,
  489. );
  490. await this.token.$_mint(this.vault.address, constants.MAX_INT256);
  491. await this.vault.$_mint(holder, constants.MAX_INT256);
  492. });
  493. it('redeem', async function () {
  494. expect(await this.vault.previewRedeem(amountWithFees)).to.be.bignumber.equal(amountWithoutFees);
  495. ({ tx: this.tx } = await this.vault.redeem(amountWithFees, recipient, holder, { from: holder }));
  496. });
  497. it('withdraw', async function () {
  498. expect(await this.vault.previewWithdraw(amountWithoutFees)).to.be.bignumber.equal(amountWithFees);
  499. ({ tx: this.tx } = await this.vault.withdraw(amountWithoutFees, recipient, holder, { from: holder }));
  500. });
  501. afterEach(async function () {
  502. // withdraw principal
  503. await expectEvent.inTransaction(this.tx, this.token, 'Transfer', {
  504. from: this.vault.address,
  505. to: recipient,
  506. value: amountWithoutFees,
  507. });
  508. // redirect fees
  509. await expectEvent.inTransaction(this.tx, this.token, 'Transfer', {
  510. from: this.vault.address,
  511. to: other,
  512. value: fees,
  513. });
  514. // mint shares
  515. await expectEvent.inTransaction(this.tx, this.vault, 'Transfer', {
  516. from: holder,
  517. to: constants.ZERO_ADDRESS,
  518. value: amountWithFees,
  519. });
  520. // withdraw event
  521. await expectEvent.inTransaction(this.tx, this.vault, 'Withdraw', {
  522. sender: holder,
  523. receiver: recipient,
  524. owner: holder,
  525. assets: amountWithoutFees,
  526. shares: amountWithFees,
  527. });
  528. });
  529. });
  530. });
  531. /// Scenario inspired by solmate ERC4626 tests:
  532. /// https://github.com/transmissions11/solmate/blob/main/src/test/ERC4626.t.sol
  533. it('multiple mint, deposit, redeem & withdrawal', async function () {
  534. // test designed with both asset using similar decimals
  535. this.token = await ERC20Decimals.new(name, symbol, 18);
  536. this.vault = await ERC4626.new(name + ' Vault', symbol + 'V', this.token.address);
  537. await this.token.$_mint(user1, 4000);
  538. await this.token.$_mint(user2, 7001);
  539. await this.token.approve(this.vault.address, 4000, { from: user1 });
  540. await this.token.approve(this.vault.address, 7001, { from: user2 });
  541. // 1. Alice mints 2000 shares (costs 2000 tokens)
  542. {
  543. const { tx } = await this.vault.mint(2000, user1, { from: user1 });
  544. await expectEvent.inTransaction(tx, this.token, 'Transfer', {
  545. from: user1,
  546. to: this.vault.address,
  547. value: '2000',
  548. });
  549. await expectEvent.inTransaction(tx, this.vault, 'Transfer', {
  550. from: constants.ZERO_ADDRESS,
  551. to: user1,
  552. value: '2000',
  553. });
  554. expect(await this.vault.previewDeposit(2000)).to.be.bignumber.equal('2000');
  555. expect(await this.vault.balanceOf(user1)).to.be.bignumber.equal('2000');
  556. expect(await this.vault.balanceOf(user2)).to.be.bignumber.equal('0');
  557. expect(await this.vault.convertToAssets(await this.vault.balanceOf(user1))).to.be.bignumber.equal('2000');
  558. expect(await this.vault.convertToAssets(await this.vault.balanceOf(user2))).to.be.bignumber.equal('0');
  559. expect(await this.vault.totalSupply()).to.be.bignumber.equal('2000');
  560. expect(await this.vault.totalAssets()).to.be.bignumber.equal('2000');
  561. }
  562. // 2. Bob deposits 4000 tokens (mints 4000 shares)
  563. {
  564. const { tx } = await this.vault.mint(4000, user2, { from: user2 });
  565. await expectEvent.inTransaction(tx, this.token, 'Transfer', {
  566. from: user2,
  567. to: this.vault.address,
  568. value: '4000',
  569. });
  570. await expectEvent.inTransaction(tx, this.vault, 'Transfer', {
  571. from: constants.ZERO_ADDRESS,
  572. to: user2,
  573. value: '4000',
  574. });
  575. expect(await this.vault.previewDeposit(4000)).to.be.bignumber.equal('4000');
  576. expect(await this.vault.balanceOf(user1)).to.be.bignumber.equal('2000');
  577. expect(await this.vault.balanceOf(user2)).to.be.bignumber.equal('4000');
  578. expect(await this.vault.convertToAssets(await this.vault.balanceOf(user1))).to.be.bignumber.equal('2000');
  579. expect(await this.vault.convertToAssets(await this.vault.balanceOf(user2))).to.be.bignumber.equal('4000');
  580. expect(await this.vault.totalSupply()).to.be.bignumber.equal('6000');
  581. expect(await this.vault.totalAssets()).to.be.bignumber.equal('6000');
  582. }
  583. // 3. Vault mutates by +3000 tokens (simulated yield returned from strategy)
  584. await this.token.$_mint(this.vault.address, 3000);
  585. expect(await this.vault.balanceOf(user1)).to.be.bignumber.equal('2000');
  586. expect(await this.vault.balanceOf(user2)).to.be.bignumber.equal('4000');
  587. expect(await this.vault.convertToAssets(await this.vault.balanceOf(user1))).to.be.bignumber.equal('2999'); // used to be 3000, but virtual assets/shares captures part of the yield
  588. expect(await this.vault.convertToAssets(await this.vault.balanceOf(user2))).to.be.bignumber.equal('5999'); // used to be 6000, but virtual assets/shares captures part of the yield
  589. expect(await this.vault.totalSupply()).to.be.bignumber.equal('6000');
  590. expect(await this.vault.totalAssets()).to.be.bignumber.equal('9000');
  591. // 4. Alice deposits 2000 tokens (mints 1333 shares)
  592. {
  593. const { tx } = await this.vault.deposit(2000, user1, { from: user1 });
  594. await expectEvent.inTransaction(tx, this.token, 'Transfer', {
  595. from: user1,
  596. to: this.vault.address,
  597. value: '2000',
  598. });
  599. await expectEvent.inTransaction(tx, this.vault, 'Transfer', {
  600. from: constants.ZERO_ADDRESS,
  601. to: user1,
  602. value: '1333',
  603. });
  604. expect(await this.vault.balanceOf(user1)).to.be.bignumber.equal('3333');
  605. expect(await this.vault.balanceOf(user2)).to.be.bignumber.equal('4000');
  606. expect(await this.vault.convertToAssets(await this.vault.balanceOf(user1))).to.be.bignumber.equal('4999');
  607. expect(await this.vault.convertToAssets(await this.vault.balanceOf(user2))).to.be.bignumber.equal('6000');
  608. expect(await this.vault.totalSupply()).to.be.bignumber.equal('7333');
  609. expect(await this.vault.totalAssets()).to.be.bignumber.equal('11000');
  610. }
  611. // 5. Bob mints 2000 shares (costs 3001 assets)
  612. // NOTE: Bob's assets spent got rounded up
  613. // NOTE: Alices's vault assets got rounded up
  614. {
  615. const { tx } = await this.vault.mint(2000, user2, { from: user2 });
  616. await expectEvent.inTransaction(tx, this.token, 'Transfer', {
  617. from: user2,
  618. to: this.vault.address,
  619. value: '3000', // used to be 3001
  620. });
  621. await expectEvent.inTransaction(tx, this.vault, 'Transfer', {
  622. from: constants.ZERO_ADDRESS,
  623. to: user2,
  624. value: '2000',
  625. });
  626. expect(await this.vault.balanceOf(user1)).to.be.bignumber.equal('3333');
  627. expect(await this.vault.balanceOf(user2)).to.be.bignumber.equal('6000');
  628. expect(await this.vault.convertToAssets(await this.vault.balanceOf(user1))).to.be.bignumber.equal('4999'); // used to be 5000
  629. expect(await this.vault.convertToAssets(await this.vault.balanceOf(user2))).to.be.bignumber.equal('9000');
  630. expect(await this.vault.totalSupply()).to.be.bignumber.equal('9333');
  631. expect(await this.vault.totalAssets()).to.be.bignumber.equal('14000'); // used to be 14001
  632. }
  633. // 6. Vault mutates by +3000 tokens
  634. // NOTE: Vault holds 17001 tokens, but sum of assetsOf() is 17000.
  635. await this.token.$_mint(this.vault.address, 3000);
  636. expect(await this.vault.balanceOf(user1)).to.be.bignumber.equal('3333');
  637. expect(await this.vault.balanceOf(user2)).to.be.bignumber.equal('6000');
  638. expect(await this.vault.convertToAssets(await this.vault.balanceOf(user1))).to.be.bignumber.equal('6070'); // used to be 6071
  639. expect(await this.vault.convertToAssets(await this.vault.balanceOf(user2))).to.be.bignumber.equal('10928'); // used to be 10929
  640. expect(await this.vault.totalSupply()).to.be.bignumber.equal('9333');
  641. expect(await this.vault.totalAssets()).to.be.bignumber.equal('17000'); // used to be 17001
  642. // 7. Alice redeem 1333 shares (2428 assets)
  643. {
  644. const { tx } = await this.vault.redeem(1333, user1, user1, { from: user1 });
  645. await expectEvent.inTransaction(tx, this.vault, 'Transfer', {
  646. from: user1,
  647. to: constants.ZERO_ADDRESS,
  648. value: '1333',
  649. });
  650. await expectEvent.inTransaction(tx, this.token, 'Transfer', {
  651. from: this.vault.address,
  652. to: user1,
  653. value: '2427', // used to be 2428
  654. });
  655. expect(await this.vault.balanceOf(user1)).to.be.bignumber.equal('2000');
  656. expect(await this.vault.balanceOf(user2)).to.be.bignumber.equal('6000');
  657. expect(await this.vault.convertToAssets(await this.vault.balanceOf(user1))).to.be.bignumber.equal('3643');
  658. expect(await this.vault.convertToAssets(await this.vault.balanceOf(user2))).to.be.bignumber.equal('10929');
  659. expect(await this.vault.totalSupply()).to.be.bignumber.equal('8000');
  660. expect(await this.vault.totalAssets()).to.be.bignumber.equal('14573');
  661. }
  662. // 8. Bob withdraws 2929 assets (1608 shares)
  663. {
  664. const { tx } = await this.vault.withdraw(2929, user2, user2, { from: user2 });
  665. await expectEvent.inTransaction(tx, this.vault, 'Transfer', {
  666. from: user2,
  667. to: constants.ZERO_ADDRESS,
  668. value: '1608',
  669. });
  670. await expectEvent.inTransaction(tx, this.token, 'Transfer', {
  671. from: this.vault.address,
  672. to: user2,
  673. value: '2929',
  674. });
  675. expect(await this.vault.balanceOf(user1)).to.be.bignumber.equal('2000');
  676. expect(await this.vault.balanceOf(user2)).to.be.bignumber.equal('4392');
  677. expect(await this.vault.convertToAssets(await this.vault.balanceOf(user1))).to.be.bignumber.equal('3643');
  678. expect(await this.vault.convertToAssets(await this.vault.balanceOf(user2))).to.be.bignumber.equal('8000');
  679. expect(await this.vault.totalSupply()).to.be.bignumber.equal('6392');
  680. expect(await this.vault.totalAssets()).to.be.bignumber.equal('11644');
  681. }
  682. // 9. Alice withdraws 3643 assets (2000 shares)
  683. // NOTE: Bob's assets have been rounded back up
  684. {
  685. const { tx } = await this.vault.withdraw(3643, user1, user1, { from: user1 });
  686. await expectEvent.inTransaction(tx, this.vault, 'Transfer', {
  687. from: user1,
  688. to: constants.ZERO_ADDRESS,
  689. value: '2000',
  690. });
  691. await expectEvent.inTransaction(tx, this.token, 'Transfer', {
  692. from: this.vault.address,
  693. to: user1,
  694. value: '3643',
  695. });
  696. expect(await this.vault.balanceOf(user1)).to.be.bignumber.equal('0');
  697. expect(await this.vault.balanceOf(user2)).to.be.bignumber.equal('4392');
  698. expect(await this.vault.convertToAssets(await this.vault.balanceOf(user1))).to.be.bignumber.equal('0');
  699. expect(await this.vault.convertToAssets(await this.vault.balanceOf(user2))).to.be.bignumber.equal('8000'); // used to be 8001
  700. expect(await this.vault.totalSupply()).to.be.bignumber.equal('4392');
  701. expect(await this.vault.totalAssets()).to.be.bignumber.equal('8001');
  702. }
  703. // 10. Bob redeem 4392 shares (8001 tokens)
  704. {
  705. const { tx } = await this.vault.redeem(4392, user2, user2, { from: user2 });
  706. await expectEvent.inTransaction(tx, this.vault, 'Transfer', {
  707. from: user2,
  708. to: constants.ZERO_ADDRESS,
  709. value: '4392',
  710. });
  711. await expectEvent.inTransaction(tx, this.token, 'Transfer', {
  712. from: this.vault.address,
  713. to: user2,
  714. value: '8000', // used to be 8001
  715. });
  716. expect(await this.vault.balanceOf(user1)).to.be.bignumber.equal('0');
  717. expect(await this.vault.balanceOf(user2)).to.be.bignumber.equal('0');
  718. expect(await this.vault.convertToAssets(await this.vault.balanceOf(user1))).to.be.bignumber.equal('0');
  719. expect(await this.vault.convertToAssets(await this.vault.balanceOf(user2))).to.be.bignumber.equal('0');
  720. expect(await this.vault.totalSupply()).to.be.bignumber.equal('0');
  721. expect(await this.vault.totalAssets()).to.be.bignumber.equal('1'); // used to be 0
  722. }
  723. });
  724. });