ERC4626.test.js 41 KB

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