ERC4626.test.js 42 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035
  1. const { constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers');
  2. const { expect } = require('chai');
  3. const { Enum } = require('../../../helpers/enums');
  4. const { expectRevertCustomError } = require('../../../helpers/customError');
  5. const ERC20Decimals = artifacts.require('$ERC20DecimalsMock');
  6. const ERC4626 = artifacts.require('$ERC4626');
  7. const ERC4626OffsetMock = artifacts.require('$ERC4626OffsetMock');
  8. const ERC4626FeesMock = artifacts.require('$ERC4626FeesMock');
  9. const ERC20ExcessDecimalsMock = artifacts.require('ERC20ExcessDecimalsMock');
  10. const ERC20Reentrant = artifacts.require('$ERC20Reentrant');
  11. contract('ERC4626', function (accounts) {
  12. const [holder, recipient, spender, other, user1, user2] = accounts;
  13. const name = 'My Token';
  14. const symbol = 'MTKN';
  15. const decimals = web3.utils.toBN(18);
  16. it('inherit decimals if from asset', async function () {
  17. for (const decimals of [0, 9, 12, 18, 36].map(web3.utils.toBN)) {
  18. const token = await ERC20Decimals.new('', '', decimals);
  19. const vault = await ERC4626.new('', '', token.address);
  20. expect(await vault.decimals()).to.be.bignumber.equal(decimals);
  21. }
  22. });
  23. it('asset has not yet been created', async function () {
  24. const vault = await ERC4626.new('', '', other);
  25. expect(await vault.decimals()).to.be.bignumber.equal(decimals);
  26. });
  27. it('underlying excess decimals', async function () {
  28. const token = await ERC20ExcessDecimalsMock.new();
  29. const vault = await ERC4626.new('', '', token.address);
  30. expect(await vault.decimals()).to.be.bignumber.equal(decimals);
  31. });
  32. it('decimals overflow', async function () {
  33. for (const offset of [243, 250, 255].map(web3.utils.toBN)) {
  34. const token = await ERC20Decimals.new('', '', decimals);
  35. const vault = await ERC4626OffsetMock.new(name + ' Vault', symbol + 'V', token.address, offset);
  36. await expectRevert(
  37. vault.decimals(),
  38. 'reverted with panic code 0x11 (Arithmetic operation underflowed or overflowed outside of an unchecked block)',
  39. );
  40. }
  41. });
  42. describe('reentrancy', async function () {
  43. const reenterType = Enum('No', 'Before', 'After');
  44. const amount = web3.utils.toBN(1000000000000000000);
  45. const reenterAmount = web3.utils.toBN(1000000000);
  46. let token;
  47. let vault;
  48. beforeEach(async function () {
  49. token = await ERC20Reentrant.new();
  50. // Use offset 1 so the rate is not 1:1 and we can't possibly confuse assets and shares
  51. vault = await ERC4626OffsetMock.new('', '', token.address, 1);
  52. // Funds and approval for tests
  53. await token.$_mint(holder, amount);
  54. await token.$_mint(other, amount);
  55. await token.$_approve(holder, vault.address, constants.MAX_UINT256);
  56. await token.$_approve(other, vault.address, constants.MAX_UINT256);
  57. await token.$_approve(token.address, vault.address, constants.MAX_UINT256);
  58. });
  59. // During a `_deposit`, the vault does `transferFrom(depositor, vault, assets)` -> `_mint(receiver, shares)`
  60. // such that a reentrancy BEFORE the transfer guarantees the price is kept the same.
  61. // If the order of transfer -> mint is changed to mint -> transfer, the reentrancy could be triggered on an
  62. // intermediate state in which the ratio of assets/shares has been decreased (more shares than assets).
  63. it('correct share price is observed during reentrancy before deposit', async function () {
  64. // mint token for deposit
  65. await token.$_mint(token.address, reenterAmount);
  66. // Schedules a reentrancy from the token contract
  67. await token.scheduleReenter(
  68. reenterType.Before,
  69. vault.address,
  70. vault.contract.methods.deposit(reenterAmount, holder).encodeABI(),
  71. );
  72. // Initial share price
  73. const sharesForDeposit = await vault.previewDeposit(amount, { from: holder });
  74. const sharesForReenter = await vault.previewDeposit(reenterAmount, { from: holder });
  75. // Do deposit normally, triggering the _beforeTokenTransfer hook
  76. const receipt = await vault.deposit(amount, holder, { from: holder });
  77. // Main deposit event
  78. await expectEvent(receipt, 'Deposit', {
  79. sender: holder,
  80. owner: holder,
  81. assets: amount,
  82. shares: sharesForDeposit,
  83. });
  84. // Reentrant deposit event → uses the same price
  85. await expectEvent(receipt, 'Deposit', {
  86. sender: token.address,
  87. owner: holder,
  88. assets: reenterAmount,
  89. shares: sharesForReenter,
  90. });
  91. // Assert prices is kept
  92. const sharesAfter = await vault.previewDeposit(amount, { from: holder });
  93. expect(sharesForDeposit).to.be.bignumber.eq(sharesAfter);
  94. });
  95. // During a `_withdraw`, the vault does `_burn(owner, shares)` -> `transfer(receiver, assets)`
  96. // such that a reentrancy AFTER the transfer guarantees the price is kept the same.
  97. // If the order of burn -> transfer is changed to transfer -> burn, the reentrancy could be triggered on an
  98. // intermediate state in which the ratio of shares/assets has been decreased (more assets than shares).
  99. it('correct share price is observed during reentrancy after withdraw', async function () {
  100. // Deposit into the vault: holder gets `amount` share, token.address gets `reenterAmount` shares
  101. await vault.deposit(amount, holder, { from: holder });
  102. await vault.deposit(reenterAmount, token.address, { from: other });
  103. // Schedules a reentrancy from the token contract
  104. await token.scheduleReenter(
  105. reenterType.After,
  106. vault.address,
  107. vault.contract.methods.withdraw(reenterAmount, holder, token.address).encodeABI(),
  108. );
  109. // Initial share price
  110. const sharesForWithdraw = await vault.previewWithdraw(amount, { from: holder });
  111. const sharesForReenter = await vault.previewWithdraw(reenterAmount, { from: holder });
  112. // Do withdraw normally, triggering the _afterTokenTransfer hook
  113. const receipt = await vault.withdraw(amount, holder, holder, { from: holder });
  114. // Main withdraw event
  115. await expectEvent(receipt, 'Withdraw', {
  116. sender: holder,
  117. receiver: holder,
  118. owner: holder,
  119. assets: amount,
  120. shares: sharesForWithdraw,
  121. });
  122. // Reentrant withdraw event → uses the same price
  123. await expectEvent(receipt, 'Withdraw', {
  124. sender: token.address,
  125. receiver: holder,
  126. owner: token.address,
  127. assets: reenterAmount,
  128. shares: sharesForReenter,
  129. });
  130. // Assert price is kept
  131. const sharesAfter = await vault.previewWithdraw(amount, { from: holder });
  132. expect(sharesForWithdraw).to.be.bignumber.eq(sharesAfter);
  133. });
  134. // Donate newly minted tokens to the vault during the reentracy causes the share price to increase.
  135. // Still, the deposit that trigger the reentracy is not affected and get the previewed price.
  136. // Further deposits will get a different price (getting fewer shares for the same amount of assets)
  137. it('share price change during reentracy does not affect deposit', async function () {
  138. // Schedules a reentrancy from the token contract that mess up the share price
  139. await token.scheduleReenter(
  140. reenterType.Before,
  141. token.address,
  142. token.contract.methods.$_mint(vault.address, reenterAmount).encodeABI(),
  143. );
  144. // Price before
  145. const sharesBefore = await vault.previewDeposit(amount);
  146. // Deposit, triggering the _beforeTokenTransfer hook
  147. const receipt = await vault.deposit(amount, holder, { from: holder });
  148. // Price is as previewed
  149. await expectEvent(receipt, 'Deposit', {
  150. sender: holder,
  151. owner: holder,
  152. assets: amount,
  153. shares: sharesBefore,
  154. });
  155. // Price was modified during reentrancy
  156. const sharesAfter = await vault.previewDeposit(amount);
  157. expect(sharesAfter).to.be.bignumber.lt(sharesBefore);
  158. });
  159. // Burn some tokens from the vault during the reentracy causes the share price to drop.
  160. // Still, the withdraw that trigger the reentracy is not affected and get the previewed price.
  161. // Further withdraw will get a different price (needing more shares for the same amount of assets)
  162. it('share price change during reentracy does not affect withdraw', async function () {
  163. await vault.deposit(amount, other, { from: other });
  164. await vault.deposit(amount, holder, { from: holder });
  165. // Schedules a reentrancy from the token contract that mess up the share price
  166. await token.scheduleReenter(
  167. reenterType.After,
  168. token.address,
  169. token.contract.methods.$_burn(vault.address, reenterAmount).encodeABI(),
  170. );
  171. // Price before
  172. const sharesBefore = await vault.previewWithdraw(amount);
  173. // Withdraw, triggering the _afterTokenTransfer hook
  174. const receipt = await vault.withdraw(amount, holder, holder, { from: holder });
  175. // Price is as previewed
  176. await expectEvent(receipt, 'Withdraw', {
  177. sender: holder,
  178. receiver: holder,
  179. owner: holder,
  180. assets: amount,
  181. shares: sharesBefore,
  182. });
  183. // Price was modified during reentrancy
  184. const sharesAfter = await vault.previewWithdraw(amount);
  185. expect(sharesAfter).to.be.bignumber.gt(sharesBefore);
  186. });
  187. });
  188. for (const offset of [0, 6, 18].map(web3.utils.toBN)) {
  189. const parseToken = token => web3.utils.toBN(10).pow(decimals).muln(token);
  190. const parseShare = share => web3.utils.toBN(10).pow(decimals.add(offset)).muln(share);
  191. const virtualAssets = web3.utils.toBN(1);
  192. const virtualShares = web3.utils.toBN(10).pow(offset);
  193. describe(`offset: ${offset}`, function () {
  194. beforeEach(async function () {
  195. this.token = await ERC20Decimals.new(name, symbol, decimals);
  196. this.vault = await ERC4626OffsetMock.new(name + ' Vault', symbol + 'V', this.token.address, offset);
  197. await this.token.$_mint(holder, constants.MAX_INT256); // 50% of maximum
  198. await this.token.approve(this.vault.address, constants.MAX_UINT256, { from: holder });
  199. await this.vault.approve(spender, constants.MAX_UINT256, { from: holder });
  200. });
  201. it('metadata', async function () {
  202. expect(await this.vault.name()).to.be.equal(name + ' Vault');
  203. expect(await this.vault.symbol()).to.be.equal(symbol + 'V');
  204. expect(await this.vault.decimals()).to.be.bignumber.equal(decimals.add(offset));
  205. expect(await this.vault.asset()).to.be.equal(this.token.address);
  206. });
  207. describe('empty vault: no assets & no shares', function () {
  208. it('status', async function () {
  209. expect(await this.vault.totalAssets()).to.be.bignumber.equal('0');
  210. });
  211. it('deposit', async function () {
  212. expect(await this.vault.maxDeposit(holder)).to.be.bignumber.equal(constants.MAX_UINT256);
  213. expect(await this.vault.previewDeposit(parseToken(1))).to.be.bignumber.equal(parseShare(1));
  214. const { tx } = await this.vault.deposit(parseToken(1), recipient, { from: holder });
  215. await expectEvent.inTransaction(tx, this.token, 'Transfer', {
  216. from: holder,
  217. to: this.vault.address,
  218. value: parseToken(1),
  219. });
  220. await expectEvent.inTransaction(tx, this.vault, 'Transfer', {
  221. from: constants.ZERO_ADDRESS,
  222. to: recipient,
  223. value: parseShare(1),
  224. });
  225. await expectEvent.inTransaction(tx, this.vault, 'Deposit', {
  226. sender: holder,
  227. owner: recipient,
  228. assets: parseToken(1),
  229. shares: parseShare(1),
  230. });
  231. });
  232. it('mint', async function () {
  233. expect(await this.vault.maxMint(holder)).to.be.bignumber.equal(constants.MAX_UINT256);
  234. expect(await this.vault.previewMint(parseShare(1))).to.be.bignumber.equal(parseToken(1));
  235. const { tx } = await this.vault.mint(parseShare(1), recipient, { from: holder });
  236. await expectEvent.inTransaction(tx, this.token, 'Transfer', {
  237. from: holder,
  238. to: this.vault.address,
  239. value: parseToken(1),
  240. });
  241. await expectEvent.inTransaction(tx, this.vault, 'Transfer', {
  242. from: constants.ZERO_ADDRESS,
  243. to: recipient,
  244. value: parseShare(1),
  245. });
  246. await expectEvent.inTransaction(tx, this.vault, 'Deposit', {
  247. sender: holder,
  248. owner: recipient,
  249. assets: parseToken(1),
  250. shares: parseShare(1),
  251. });
  252. });
  253. it('withdraw', async function () {
  254. expect(await this.vault.maxWithdraw(holder)).to.be.bignumber.equal('0');
  255. expect(await this.vault.previewWithdraw('0')).to.be.bignumber.equal('0');
  256. const { tx } = await this.vault.withdraw('0', recipient, holder, { from: holder });
  257. await expectEvent.inTransaction(tx, this.token, 'Transfer', {
  258. from: this.vault.address,
  259. to: recipient,
  260. value: '0',
  261. });
  262. await expectEvent.inTransaction(tx, this.vault, 'Transfer', {
  263. from: holder,
  264. to: constants.ZERO_ADDRESS,
  265. value: '0',
  266. });
  267. await expectEvent.inTransaction(tx, this.vault, 'Withdraw', {
  268. sender: holder,
  269. receiver: recipient,
  270. owner: holder,
  271. assets: '0',
  272. shares: '0',
  273. });
  274. });
  275. it('redeem', async function () {
  276. expect(await this.vault.maxRedeem(holder)).to.be.bignumber.equal('0');
  277. expect(await this.vault.previewRedeem('0')).to.be.bignumber.equal('0');
  278. const { tx } = await this.vault.redeem('0', recipient, holder, { from: holder });
  279. await expectEvent.inTransaction(tx, this.token, 'Transfer', {
  280. from: this.vault.address,
  281. to: recipient,
  282. value: '0',
  283. });
  284. await expectEvent.inTransaction(tx, this.vault, 'Transfer', {
  285. from: holder,
  286. to: constants.ZERO_ADDRESS,
  287. value: '0',
  288. });
  289. await expectEvent.inTransaction(tx, this.vault, 'Withdraw', {
  290. sender: holder,
  291. receiver: recipient,
  292. owner: holder,
  293. assets: '0',
  294. shares: '0',
  295. });
  296. });
  297. });
  298. describe('inflation attack: offset price by direct deposit of assets', function () {
  299. beforeEach(async function () {
  300. // Donate 1 token to the vault to offset the price
  301. await this.token.$_mint(this.vault.address, parseToken(1));
  302. });
  303. it('status', async function () {
  304. expect(await this.vault.totalSupply()).to.be.bignumber.equal('0');
  305. expect(await this.vault.totalAssets()).to.be.bignumber.equal(parseToken(1));
  306. });
  307. /**
  308. * | offset | deposited assets | redeemable assets |
  309. * |--------|----------------------|----------------------|
  310. * | 0 | 1.000000000000000000 | 0. |
  311. * | 6 | 1.000000000000000000 | 0.999999000000000000 |
  312. * | 18 | 1.000000000000000000 | 0.999999999999999999 |
  313. *
  314. * Attack is possible, but made difficult by the offset. For the attack to be successful
  315. * the attacker needs to frontrun a deposit 10**offset times bigger than what the victim
  316. * was trying to deposit
  317. */
  318. it('deposit', async function () {
  319. const effectiveAssets = await this.vault.totalAssets().then(x => x.add(virtualAssets));
  320. const effectiveShares = await this.vault.totalSupply().then(x => x.add(virtualShares));
  321. const depositAssets = parseToken(1);
  322. const expectedShares = depositAssets.mul(effectiveShares).div(effectiveAssets);
  323. expect(await this.vault.maxDeposit(holder)).to.be.bignumber.equal(constants.MAX_UINT256);
  324. expect(await this.vault.previewDeposit(depositAssets)).to.be.bignumber.equal(expectedShares);
  325. const { tx } = await this.vault.deposit(depositAssets, recipient, { from: holder });
  326. await expectEvent.inTransaction(tx, this.token, 'Transfer', {
  327. from: holder,
  328. to: this.vault.address,
  329. value: depositAssets,
  330. });
  331. await expectEvent.inTransaction(tx, this.vault, 'Transfer', {
  332. from: constants.ZERO_ADDRESS,
  333. to: recipient,
  334. value: expectedShares,
  335. });
  336. await expectEvent.inTransaction(tx, this.vault, 'Deposit', {
  337. sender: holder,
  338. owner: recipient,
  339. assets: depositAssets,
  340. shares: expectedShares,
  341. });
  342. });
  343. /**
  344. * | offset | deposited assets | redeemable assets |
  345. * |--------|----------------------|----------------------|
  346. * | 0 | 1000000000000000001. | 1000000000000000001. |
  347. * | 6 | 1000000000000000001. | 1000000000000000001. |
  348. * | 18 | 1000000000000000001. | 1000000000000000001. |
  349. *
  350. * Using mint protects against inflation attack, but makes minting shares very expensive.
  351. * The ER20 allowance for the underlying asset is needed to protect the user from (too)
  352. * large deposits.
  353. */
  354. it('mint', async function () {
  355. const effectiveAssets = await this.vault.totalAssets().then(x => x.add(virtualAssets));
  356. const effectiveShares = await this.vault.totalSupply().then(x => x.add(virtualShares));
  357. const mintShares = parseShare(1);
  358. const expectedAssets = mintShares.mul(effectiveAssets).div(effectiveShares);
  359. expect(await this.vault.maxMint(holder)).to.be.bignumber.equal(constants.MAX_UINT256);
  360. expect(await this.vault.previewMint(mintShares)).to.be.bignumber.equal(expectedAssets);
  361. const { tx } = await this.vault.mint(mintShares, recipient, { from: holder });
  362. await expectEvent.inTransaction(tx, this.token, 'Transfer', {
  363. from: holder,
  364. to: this.vault.address,
  365. value: expectedAssets,
  366. });
  367. await expectEvent.inTransaction(tx, this.vault, 'Transfer', {
  368. from: constants.ZERO_ADDRESS,
  369. to: recipient,
  370. value: mintShares,
  371. });
  372. await expectEvent.inTransaction(tx, this.vault, 'Deposit', {
  373. sender: holder,
  374. owner: recipient,
  375. assets: expectedAssets,
  376. shares: mintShares,
  377. });
  378. });
  379. it('withdraw', async function () {
  380. expect(await this.vault.maxWithdraw(holder)).to.be.bignumber.equal('0');
  381. expect(await this.vault.previewWithdraw('0')).to.be.bignumber.equal('0');
  382. const { tx } = await this.vault.withdraw('0', recipient, holder, { from: holder });
  383. await expectEvent.inTransaction(tx, this.token, 'Transfer', {
  384. from: this.vault.address,
  385. to: recipient,
  386. value: '0',
  387. });
  388. await expectEvent.inTransaction(tx, this.vault, 'Transfer', {
  389. from: holder,
  390. to: constants.ZERO_ADDRESS,
  391. value: '0',
  392. });
  393. await expectEvent.inTransaction(tx, this.vault, 'Withdraw', {
  394. sender: holder,
  395. receiver: recipient,
  396. owner: holder,
  397. assets: '0',
  398. shares: '0',
  399. });
  400. });
  401. it('redeem', async function () {
  402. expect(await this.vault.maxRedeem(holder)).to.be.bignumber.equal('0');
  403. expect(await this.vault.previewRedeem('0')).to.be.bignumber.equal('0');
  404. const { tx } = await this.vault.redeem('0', recipient, holder, { from: holder });
  405. await expectEvent.inTransaction(tx, this.token, 'Transfer', {
  406. from: this.vault.address,
  407. to: recipient,
  408. value: '0',
  409. });
  410. await expectEvent.inTransaction(tx, this.vault, 'Transfer', {
  411. from: holder,
  412. to: constants.ZERO_ADDRESS,
  413. value: '0',
  414. });
  415. await expectEvent.inTransaction(tx, this.vault, 'Withdraw', {
  416. sender: holder,
  417. receiver: recipient,
  418. owner: holder,
  419. assets: '0',
  420. shares: '0',
  421. });
  422. });
  423. });
  424. describe('full vault: assets & shares', function () {
  425. beforeEach(async function () {
  426. // Add 1 token of underlying asset and 100 shares to the vault
  427. await this.token.$_mint(this.vault.address, parseToken(1));
  428. await this.vault.$_mint(holder, parseShare(100));
  429. });
  430. it('status', async function () {
  431. expect(await this.vault.totalSupply()).to.be.bignumber.equal(parseShare(100));
  432. expect(await this.vault.totalAssets()).to.be.bignumber.equal(parseToken(1));
  433. });
  434. /**
  435. * | offset | deposited assets | redeemable assets |
  436. * |--------|--------------------- |----------------------|
  437. * | 0 | 1.000000000000000000 | 0.999999999999999999 |
  438. * | 6 | 1.000000000000000000 | 0.999999999999999999 |
  439. * | 18 | 1.000000000000000000 | 0.999999999999999999 |
  440. *
  441. * Virtual shares & assets captures part of the value
  442. */
  443. it('deposit', async function () {
  444. const effectiveAssets = await this.vault.totalAssets().then(x => x.add(virtualAssets));
  445. const effectiveShares = await this.vault.totalSupply().then(x => x.add(virtualShares));
  446. const depositAssets = parseToken(1);
  447. const expectedShares = depositAssets.mul(effectiveShares).div(effectiveAssets);
  448. expect(await this.vault.maxDeposit(holder)).to.be.bignumber.equal(constants.MAX_UINT256);
  449. expect(await this.vault.previewDeposit(depositAssets)).to.be.bignumber.equal(expectedShares);
  450. const { tx } = await this.vault.deposit(depositAssets, recipient, { from: holder });
  451. await expectEvent.inTransaction(tx, this.token, 'Transfer', {
  452. from: holder,
  453. to: this.vault.address,
  454. value: depositAssets,
  455. });
  456. await expectEvent.inTransaction(tx, this.vault, 'Transfer', {
  457. from: constants.ZERO_ADDRESS,
  458. to: recipient,
  459. value: expectedShares,
  460. });
  461. await expectEvent.inTransaction(tx, this.vault, 'Deposit', {
  462. sender: holder,
  463. owner: recipient,
  464. assets: depositAssets,
  465. shares: expectedShares,
  466. });
  467. });
  468. /**
  469. * | offset | deposited assets | redeemable assets |
  470. * |--------|--------------------- |----------------------|
  471. * | 0 | 0.010000000000000001 | 0.010000000000000000 |
  472. * | 6 | 0.010000000000000001 | 0.010000000000000000 |
  473. * | 18 | 0.010000000000000001 | 0.010000000000000000 |
  474. *
  475. * Virtual shares & assets captures part of the value
  476. */
  477. it('mint', async function () {
  478. const effectiveAssets = await this.vault.totalAssets().then(x => x.add(virtualAssets));
  479. const effectiveShares = await this.vault.totalSupply().then(x => x.add(virtualShares));
  480. const mintShares = parseShare(1);
  481. const expectedAssets = mintShares.mul(effectiveAssets).div(effectiveShares).addn(1); // add for the rounding
  482. expect(await this.vault.maxMint(holder)).to.be.bignumber.equal(constants.MAX_UINT256);
  483. expect(await this.vault.previewMint(mintShares)).to.be.bignumber.equal(expectedAssets);
  484. const { tx } = await this.vault.mint(mintShares, recipient, { from: holder });
  485. await expectEvent.inTransaction(tx, this.token, 'Transfer', {
  486. from: holder,
  487. to: this.vault.address,
  488. value: expectedAssets,
  489. });
  490. await expectEvent.inTransaction(tx, this.vault, 'Transfer', {
  491. from: constants.ZERO_ADDRESS,
  492. to: recipient,
  493. value: mintShares,
  494. });
  495. await expectEvent.inTransaction(tx, this.vault, 'Deposit', {
  496. sender: holder,
  497. owner: recipient,
  498. assets: expectedAssets,
  499. shares: mintShares,
  500. });
  501. });
  502. it('withdraw', async function () {
  503. const effectiveAssets = await this.vault.totalAssets().then(x => x.add(virtualAssets));
  504. const effectiveShares = await this.vault.totalSupply().then(x => x.add(virtualShares));
  505. const withdrawAssets = parseToken(1);
  506. const expectedShares = withdrawAssets.mul(effectiveShares).div(effectiveAssets).addn(1); // add for the rounding
  507. expect(await this.vault.maxWithdraw(holder)).to.be.bignumber.equal(withdrawAssets);
  508. expect(await this.vault.previewWithdraw(withdrawAssets)).to.be.bignumber.equal(expectedShares);
  509. const { tx } = await this.vault.withdraw(withdrawAssets, recipient, holder, { from: holder });
  510. await expectEvent.inTransaction(tx, this.token, 'Transfer', {
  511. from: this.vault.address,
  512. to: recipient,
  513. value: withdrawAssets,
  514. });
  515. await expectEvent.inTransaction(tx, this.vault, 'Transfer', {
  516. from: holder,
  517. to: constants.ZERO_ADDRESS,
  518. value: expectedShares,
  519. });
  520. await expectEvent.inTransaction(tx, this.vault, 'Withdraw', {
  521. sender: holder,
  522. receiver: recipient,
  523. owner: holder,
  524. assets: withdrawAssets,
  525. shares: expectedShares,
  526. });
  527. });
  528. it('withdraw with approval', async function () {
  529. const assets = await this.vault.previewWithdraw(parseToken(1));
  530. await expectRevertCustomError(
  531. this.vault.withdraw(parseToken(1), recipient, holder, { from: other }),
  532. 'ERC20InsufficientAllowance',
  533. [other, 0, assets],
  534. );
  535. await this.vault.withdraw(parseToken(1), recipient, holder, { from: spender });
  536. });
  537. it('redeem', async function () {
  538. const effectiveAssets = await this.vault.totalAssets().then(x => x.add(virtualAssets));
  539. const effectiveShares = await this.vault.totalSupply().then(x => x.add(virtualShares));
  540. const redeemShares = parseShare(100);
  541. const expectedAssets = redeemShares.mul(effectiveAssets).div(effectiveShares);
  542. expect(await this.vault.maxRedeem(holder)).to.be.bignumber.equal(redeemShares);
  543. expect(await this.vault.previewRedeem(redeemShares)).to.be.bignumber.equal(expectedAssets);
  544. const { tx } = await this.vault.redeem(redeemShares, recipient, holder, { from: holder });
  545. await expectEvent.inTransaction(tx, this.token, 'Transfer', {
  546. from: this.vault.address,
  547. to: recipient,
  548. value: expectedAssets,
  549. });
  550. await expectEvent.inTransaction(tx, this.vault, 'Transfer', {
  551. from: holder,
  552. to: constants.ZERO_ADDRESS,
  553. value: redeemShares,
  554. });
  555. await expectEvent.inTransaction(tx, this.vault, 'Withdraw', {
  556. sender: holder,
  557. receiver: recipient,
  558. owner: holder,
  559. assets: expectedAssets,
  560. shares: redeemShares,
  561. });
  562. });
  563. it('redeem with approval', async function () {
  564. await expectRevertCustomError(
  565. this.vault.redeem(parseShare(100), recipient, holder, { from: other }),
  566. 'ERC20InsufficientAllowance',
  567. [other, 0, parseShare(100)],
  568. );
  569. await this.vault.redeem(parseShare(100), recipient, holder, { from: spender });
  570. });
  571. });
  572. });
  573. }
  574. describe('ERC4626Fees', function () {
  575. const feeBasePoint = web3.utils.toBN(5e3);
  576. const amountWithoutFees = web3.utils.toBN(10000);
  577. const fees = amountWithoutFees.mul(feeBasePoint).divn(1e5);
  578. const amountWithFees = amountWithoutFees.add(fees);
  579. describe('input fees', function () {
  580. beforeEach(async function () {
  581. this.token = await ERC20Decimals.new(name, symbol, 18);
  582. this.vault = await ERC4626FeesMock.new(
  583. name + ' Vault',
  584. symbol + 'V',
  585. this.token.address,
  586. feeBasePoint,
  587. other,
  588. 0,
  589. constants.ZERO_ADDRESS,
  590. );
  591. await this.token.$_mint(holder, constants.MAX_INT256);
  592. await this.token.approve(this.vault.address, constants.MAX_INT256, { from: holder });
  593. });
  594. it('deposit', async function () {
  595. expect(await this.vault.previewDeposit(amountWithFees)).to.be.bignumber.equal(amountWithoutFees);
  596. ({ tx: this.tx } = await this.vault.deposit(amountWithFees, recipient, { from: holder }));
  597. });
  598. it('mint', async function () {
  599. expect(await this.vault.previewMint(amountWithoutFees)).to.be.bignumber.equal(amountWithFees);
  600. ({ tx: this.tx } = await this.vault.mint(amountWithoutFees, recipient, { from: holder }));
  601. });
  602. afterEach(async function () {
  603. // get total
  604. await expectEvent.inTransaction(this.tx, this.token, 'Transfer', {
  605. from: holder,
  606. to: this.vault.address,
  607. value: amountWithFees,
  608. });
  609. // redirect fees
  610. await expectEvent.inTransaction(this.tx, this.token, 'Transfer', {
  611. from: this.vault.address,
  612. to: other,
  613. value: fees,
  614. });
  615. // mint shares
  616. await expectEvent.inTransaction(this.tx, this.vault, 'Transfer', {
  617. from: constants.ZERO_ADDRESS,
  618. to: recipient,
  619. value: amountWithoutFees,
  620. });
  621. // deposit event
  622. await expectEvent.inTransaction(this.tx, this.vault, 'Deposit', {
  623. sender: holder,
  624. owner: recipient,
  625. assets: amountWithFees,
  626. shares: amountWithoutFees,
  627. });
  628. });
  629. });
  630. describe('output fees', function () {
  631. beforeEach(async function () {
  632. this.token = await ERC20Decimals.new(name, symbol, 18);
  633. this.vault = await ERC4626FeesMock.new(
  634. name + ' Vault',
  635. symbol + 'V',
  636. this.token.address,
  637. 0,
  638. constants.ZERO_ADDRESS,
  639. 5e3, // 5%
  640. other,
  641. );
  642. await this.token.$_mint(this.vault.address, constants.MAX_INT256);
  643. await this.vault.$_mint(holder, constants.MAX_INT256);
  644. });
  645. it('redeem', async function () {
  646. expect(await this.vault.previewRedeem(amountWithFees)).to.be.bignumber.equal(amountWithoutFees);
  647. ({ tx: this.tx } = await this.vault.redeem(amountWithFees, recipient, holder, { from: holder }));
  648. });
  649. it('withdraw', async function () {
  650. expect(await this.vault.previewWithdraw(amountWithoutFees)).to.be.bignumber.equal(amountWithFees);
  651. ({ tx: this.tx } = await this.vault.withdraw(amountWithoutFees, recipient, holder, { from: holder }));
  652. });
  653. afterEach(async function () {
  654. // withdraw principal
  655. await expectEvent.inTransaction(this.tx, this.token, 'Transfer', {
  656. from: this.vault.address,
  657. to: recipient,
  658. value: amountWithoutFees,
  659. });
  660. // redirect fees
  661. await expectEvent.inTransaction(this.tx, this.token, 'Transfer', {
  662. from: this.vault.address,
  663. to: other,
  664. value: fees,
  665. });
  666. // mint shares
  667. await expectEvent.inTransaction(this.tx, this.vault, 'Transfer', {
  668. from: holder,
  669. to: constants.ZERO_ADDRESS,
  670. value: amountWithFees,
  671. });
  672. // withdraw event
  673. await expectEvent.inTransaction(this.tx, this.vault, 'Withdraw', {
  674. sender: holder,
  675. receiver: recipient,
  676. owner: holder,
  677. assets: amountWithoutFees,
  678. shares: amountWithFees,
  679. });
  680. });
  681. });
  682. });
  683. /// Scenario inspired by solmate ERC4626 tests:
  684. /// https://github.com/transmissions11/solmate/blob/main/src/test/ERC4626.t.sol
  685. it('multiple mint, deposit, redeem & withdrawal', async function () {
  686. // test designed with both asset using similar decimals
  687. this.token = await ERC20Decimals.new(name, symbol, 18);
  688. this.vault = await ERC4626.new(name + ' Vault', symbol + 'V', this.token.address);
  689. await this.token.$_mint(user1, 4000);
  690. await this.token.$_mint(user2, 7001);
  691. await this.token.approve(this.vault.address, 4000, { from: user1 });
  692. await this.token.approve(this.vault.address, 7001, { from: user2 });
  693. // 1. Alice mints 2000 shares (costs 2000 tokens)
  694. {
  695. const { tx } = await this.vault.mint(2000, user1, { from: user1 });
  696. await expectEvent.inTransaction(tx, this.token, 'Transfer', {
  697. from: user1,
  698. to: this.vault.address,
  699. value: '2000',
  700. });
  701. await expectEvent.inTransaction(tx, this.vault, 'Transfer', {
  702. from: constants.ZERO_ADDRESS,
  703. to: user1,
  704. value: '2000',
  705. });
  706. expect(await this.vault.previewDeposit(2000)).to.be.bignumber.equal('2000');
  707. expect(await this.vault.balanceOf(user1)).to.be.bignumber.equal('2000');
  708. expect(await this.vault.balanceOf(user2)).to.be.bignumber.equal('0');
  709. expect(await this.vault.convertToAssets(await this.vault.balanceOf(user1))).to.be.bignumber.equal('2000');
  710. expect(await this.vault.convertToAssets(await this.vault.balanceOf(user2))).to.be.bignumber.equal('0');
  711. expect(await this.vault.totalSupply()).to.be.bignumber.equal('2000');
  712. expect(await this.vault.totalAssets()).to.be.bignumber.equal('2000');
  713. }
  714. // 2. Bob deposits 4000 tokens (mints 4000 shares)
  715. {
  716. const { tx } = await this.vault.mint(4000, user2, { from: user2 });
  717. await expectEvent.inTransaction(tx, this.token, 'Transfer', {
  718. from: user2,
  719. to: this.vault.address,
  720. value: '4000',
  721. });
  722. await expectEvent.inTransaction(tx, this.vault, 'Transfer', {
  723. from: constants.ZERO_ADDRESS,
  724. to: user2,
  725. value: '4000',
  726. });
  727. expect(await this.vault.previewDeposit(4000)).to.be.bignumber.equal('4000');
  728. expect(await this.vault.balanceOf(user1)).to.be.bignumber.equal('2000');
  729. expect(await this.vault.balanceOf(user2)).to.be.bignumber.equal('4000');
  730. expect(await this.vault.convertToAssets(await this.vault.balanceOf(user1))).to.be.bignumber.equal('2000');
  731. expect(await this.vault.convertToAssets(await this.vault.balanceOf(user2))).to.be.bignumber.equal('4000');
  732. expect(await this.vault.totalSupply()).to.be.bignumber.equal('6000');
  733. expect(await this.vault.totalAssets()).to.be.bignumber.equal('6000');
  734. }
  735. // 3. Vault mutates by +3000 tokens (simulated yield returned from strategy)
  736. await this.token.$_mint(this.vault.address, 3000);
  737. expect(await this.vault.balanceOf(user1)).to.be.bignumber.equal('2000');
  738. expect(await this.vault.balanceOf(user2)).to.be.bignumber.equal('4000');
  739. 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
  740. 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
  741. expect(await this.vault.totalSupply()).to.be.bignumber.equal('6000');
  742. expect(await this.vault.totalAssets()).to.be.bignumber.equal('9000');
  743. // 4. Alice deposits 2000 tokens (mints 1333 shares)
  744. {
  745. const { tx } = await this.vault.deposit(2000, user1, { from: user1 });
  746. await expectEvent.inTransaction(tx, this.token, 'Transfer', {
  747. from: user1,
  748. to: this.vault.address,
  749. value: '2000',
  750. });
  751. await expectEvent.inTransaction(tx, this.vault, 'Transfer', {
  752. from: constants.ZERO_ADDRESS,
  753. to: user1,
  754. value: '1333',
  755. });
  756. expect(await this.vault.balanceOf(user1)).to.be.bignumber.equal('3333');
  757. expect(await this.vault.balanceOf(user2)).to.be.bignumber.equal('4000');
  758. expect(await this.vault.convertToAssets(await this.vault.balanceOf(user1))).to.be.bignumber.equal('4999');
  759. expect(await this.vault.convertToAssets(await this.vault.balanceOf(user2))).to.be.bignumber.equal('6000');
  760. expect(await this.vault.totalSupply()).to.be.bignumber.equal('7333');
  761. expect(await this.vault.totalAssets()).to.be.bignumber.equal('11000');
  762. }
  763. // 5. Bob mints 2000 shares (costs 3001 assets)
  764. // NOTE: Bob's assets spent got rounded up
  765. // NOTE: Alices's vault assets got rounded up
  766. {
  767. const { tx } = await this.vault.mint(2000, user2, { from: user2 });
  768. await expectEvent.inTransaction(tx, this.token, 'Transfer', {
  769. from: user2,
  770. to: this.vault.address,
  771. value: '3000', // used to be 3001
  772. });
  773. await expectEvent.inTransaction(tx, this.vault, 'Transfer', {
  774. from: constants.ZERO_ADDRESS,
  775. to: user2,
  776. value: '2000',
  777. });
  778. expect(await this.vault.balanceOf(user1)).to.be.bignumber.equal('3333');
  779. expect(await this.vault.balanceOf(user2)).to.be.bignumber.equal('6000');
  780. expect(await this.vault.convertToAssets(await this.vault.balanceOf(user1))).to.be.bignumber.equal('4999'); // used to be 5000
  781. expect(await this.vault.convertToAssets(await this.vault.balanceOf(user2))).to.be.bignumber.equal('9000');
  782. expect(await this.vault.totalSupply()).to.be.bignumber.equal('9333');
  783. expect(await this.vault.totalAssets()).to.be.bignumber.equal('14000'); // used to be 14001
  784. }
  785. // 6. Vault mutates by +3000 tokens
  786. // NOTE: Vault holds 17001 tokens, but sum of assetsOf() is 17000.
  787. await this.token.$_mint(this.vault.address, 3000);
  788. expect(await this.vault.balanceOf(user1)).to.be.bignumber.equal('3333');
  789. expect(await this.vault.balanceOf(user2)).to.be.bignumber.equal('6000');
  790. expect(await this.vault.convertToAssets(await this.vault.balanceOf(user1))).to.be.bignumber.equal('6070'); // used to be 6071
  791. expect(await this.vault.convertToAssets(await this.vault.balanceOf(user2))).to.be.bignumber.equal('10928'); // used to be 10929
  792. expect(await this.vault.totalSupply()).to.be.bignumber.equal('9333');
  793. expect(await this.vault.totalAssets()).to.be.bignumber.equal('17000'); // used to be 17001
  794. // 7. Alice redeem 1333 shares (2428 assets)
  795. {
  796. const { tx } = await this.vault.redeem(1333, user1, user1, { from: user1 });
  797. await expectEvent.inTransaction(tx, this.vault, 'Transfer', {
  798. from: user1,
  799. to: constants.ZERO_ADDRESS,
  800. value: '1333',
  801. });
  802. await expectEvent.inTransaction(tx, this.token, 'Transfer', {
  803. from: this.vault.address,
  804. to: user1,
  805. value: '2427', // used to be 2428
  806. });
  807. expect(await this.vault.balanceOf(user1)).to.be.bignumber.equal('2000');
  808. expect(await this.vault.balanceOf(user2)).to.be.bignumber.equal('6000');
  809. expect(await this.vault.convertToAssets(await this.vault.balanceOf(user1))).to.be.bignumber.equal('3643');
  810. expect(await this.vault.convertToAssets(await this.vault.balanceOf(user2))).to.be.bignumber.equal('10929');
  811. expect(await this.vault.totalSupply()).to.be.bignumber.equal('8000');
  812. expect(await this.vault.totalAssets()).to.be.bignumber.equal('14573');
  813. }
  814. // 8. Bob withdraws 2929 assets (1608 shares)
  815. {
  816. const { tx } = await this.vault.withdraw(2929, user2, user2, { from: user2 });
  817. await expectEvent.inTransaction(tx, this.vault, 'Transfer', {
  818. from: user2,
  819. to: constants.ZERO_ADDRESS,
  820. value: '1608',
  821. });
  822. await expectEvent.inTransaction(tx, this.token, 'Transfer', {
  823. from: this.vault.address,
  824. to: user2,
  825. value: '2929',
  826. });
  827. expect(await this.vault.balanceOf(user1)).to.be.bignumber.equal('2000');
  828. expect(await this.vault.balanceOf(user2)).to.be.bignumber.equal('4392');
  829. expect(await this.vault.convertToAssets(await this.vault.balanceOf(user1))).to.be.bignumber.equal('3643');
  830. expect(await this.vault.convertToAssets(await this.vault.balanceOf(user2))).to.be.bignumber.equal('8000');
  831. expect(await this.vault.totalSupply()).to.be.bignumber.equal('6392');
  832. expect(await this.vault.totalAssets()).to.be.bignumber.equal('11644');
  833. }
  834. // 9. Alice withdraws 3643 assets (2000 shares)
  835. // NOTE: Bob's assets have been rounded back up
  836. {
  837. const { tx } = await this.vault.withdraw(3643, user1, user1, { from: user1 });
  838. await expectEvent.inTransaction(tx, this.vault, 'Transfer', {
  839. from: user1,
  840. to: constants.ZERO_ADDRESS,
  841. value: '2000',
  842. });
  843. await expectEvent.inTransaction(tx, this.token, 'Transfer', {
  844. from: this.vault.address,
  845. to: user1,
  846. value: '3643',
  847. });
  848. expect(await this.vault.balanceOf(user1)).to.be.bignumber.equal('0');
  849. expect(await this.vault.balanceOf(user2)).to.be.bignumber.equal('4392');
  850. expect(await this.vault.convertToAssets(await this.vault.balanceOf(user1))).to.be.bignumber.equal('0');
  851. expect(await this.vault.convertToAssets(await this.vault.balanceOf(user2))).to.be.bignumber.equal('8000'); // used to be 8001
  852. expect(await this.vault.totalSupply()).to.be.bignumber.equal('4392');
  853. expect(await this.vault.totalAssets()).to.be.bignumber.equal('8001');
  854. }
  855. // 10. Bob redeem 4392 shares (8001 tokens)
  856. {
  857. const { tx } = await this.vault.redeem(4392, user2, user2, { from: user2 });
  858. await expectEvent.inTransaction(tx, this.vault, 'Transfer', {
  859. from: user2,
  860. to: constants.ZERO_ADDRESS,
  861. value: '4392',
  862. });
  863. await expectEvent.inTransaction(tx, this.token, 'Transfer', {
  864. from: this.vault.address,
  865. to: user2,
  866. value: '8000', // used to be 8001
  867. });
  868. expect(await this.vault.balanceOf(user1)).to.be.bignumber.equal('0');
  869. expect(await this.vault.balanceOf(user2)).to.be.bignumber.equal('0');
  870. expect(await this.vault.convertToAssets(await this.vault.balanceOf(user1))).to.be.bignumber.equal('0');
  871. expect(await this.vault.convertToAssets(await this.vault.balanceOf(user2))).to.be.bignumber.equal('0');
  872. expect(await this.vault.totalSupply()).to.be.bignumber.equal('0');
  873. expect(await this.vault.totalAssets()).to.be.bignumber.equal('1'); // used to be 0
  874. }
  875. });
  876. });