ERC4626.test.js 42 KB

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