ERC4626.test.js 33 KB

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