diff --git a/test/allow-by-sig-test.ts b/test/allow-by-sig-test.ts index 82195d7e6..5b6fbf0e9 100644 --- a/test/allow-by-sig-test.ts +++ b/test/allow-by-sig-test.ts @@ -1,22 +1,8 @@ -import { Comet, ethers, event, expect, makeProtocol, wait } from './helpers'; -import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; +import { CometHarnessInterfaceExtendedAssetList, FaucetToken, SimplePriceFeed } from 'build/types'; +import { ethers, expect, exp, makeProtocol, wait, event, defaultAssets, SnapshotRestorer, takeSnapshot } from './helpers'; +import { SignerWithAddress } from '@nomicfoundation/hardhat-ethers/signers'; import { BigNumber, Signature } from 'ethers'; -let comet: Comet; -let _admin: SignerWithAddress; -let pauseGuardian: SignerWithAddress; -let signer: SignerWithAddress; -let manager: SignerWithAddress; -let domain; -let signature: Signature; -let signatureArgs: { - owner: string; - manager: string; - isAllowed: boolean; - nonce: BigNumber; - expiry: number; -}; - const types = { Authorization: [ { name: 'owner', type: 'address' }, @@ -28,9 +14,67 @@ const types = { }; describe('allowBySig', function () { - beforeEach(async () => { - comet = (await makeProtocol()).comet; - [_admin, pauseGuardian, signer, manager] = await ethers.getSigners(); + const baseTokenDecimals = 6; + const seedAmount = BigNumber.from(exp(10_000, baseTokenDecimals)); + const supplyAmount = BigNumber.from(exp(100, baseTokenDecimals)); + + let comet: CometHarnessInterfaceExtendedAssetList; + let baseToken: FaucetToken; + let collaterals: { [symbol: string]: FaucetToken } = {}; + let priceFeeds: { [symbol: string]: SimplePriceFeed } = {}; + + let alice: SignerWithAddress; + let bob: SignerWithAddress; + + let snapshot: SnapshotRestorer; + let snapshotWithoutAllow: SnapshotRestorer; + + let pauseGuardian: SignerWithAddress; + let domain: { + name: string; + version: string; + chainId: number; + verifyingContract: string; + }; + let signature: Signature; + let signatureArgs: { + owner: string; + manager: string; + isAllowed: boolean; + nonce: BigNumber; + expiry: number; + }; + before(async () => { + const protocol = await makeProtocol({ + base: 'USDC', + assets: defaultAssets({}, { + WETH: { + decimals: 18, + borrowCF: exp(0.8, 18), + liquidateCF: exp(0.95, 18), + liquidationFactor: exp(0.95, 18), + }, + }), + }); + comet = protocol.cometWithExtendedAssetList; + baseToken = protocol.tokens.USDC as FaucetToken; + for (const asset in protocol.tokens) { + if (asset === 'USDC') continue; + collaterals[asset] = protocol.tokens[asset] as FaucetToken; + } + for (const asset in protocol.priceFeeds) { + priceFeeds[asset] = protocol.priceFeeds[asset]; + } + [alice, bob] = protocol.users; + pauseGuardian = protocol.pauseGuardian; + + // Seed reserves so borrowing is possible + await baseToken.allocateTo(comet.address, seedAmount); + + // Alice supplies some USDC in the initial snapshot + await baseToken.allocateTo(alice.address, supplyAmount); + await baseToken.connect(alice).approve(comet.address, supplyAmount); + await comet.connect(alice).supply(baseToken.address, supplyAmount); domain = { name: await comet.name(), @@ -42,300 +86,332 @@ describe('allowBySig', function () { const timestamp = (await ethers.provider.getBlock(blockNumber)).timestamp; signatureArgs = { - owner: signer.address, - manager: manager.address, + owner: alice.address, + manager: bob.address, isAllowed: true, - nonce: await comet.userNonce(signer.address), + nonce: await comet.userNonce(alice.address), expiry: timestamp + 10, }; - const rawSignature = await signer._signTypedData(domain, types, signatureArgs); + const rawSignature = await alice._signTypedData(domain, types, signatureArgs); signature = ethers.utils.splitSignature(rawSignature); + + snapshotWithoutAllow = await takeSnapshot(); }); - it('authorizes with a valid signature', async () => { - expect(await comet.isAllowed(signer.address, manager.address)).to.be.false; - - const tx = await wait(comet - .connect(manager) - .allowBySig( - signatureArgs.owner, - signatureArgs.manager, - signatureArgs.isAllowed, - signatureArgs.nonce, - signatureArgs.expiry, - signature.v, - signature.r, - signature.s - )); - - // authorizes manager - expect(await comet.isAllowed(signer.address, manager.address)).to.be.true; - - // increments nonce - expect(await comet.userNonce(signer.address)).to.equal(signatureArgs.nonce.add(1)); - - expect(event(tx, 0)).to.be.deep.equal({ - Approval: { - owner: signer.address, - spender: manager.address, - amount: ethers.constants.MaxUint256.toBigInt(), - } + describe('positive cases', function () { + describe('allow interactions', function () { + + after(async function () { + snapshot = await takeSnapshot(); + }); + + it('authorizes with a valid signature', async () => { + expect(await comet.isAllowed(alice.address, bob.address)).to.be.false; + + const tx = await wait(comet + .connect(bob) + .allowBySig( + signatureArgs.owner, + signatureArgs.manager, + signatureArgs.isAllowed, + signatureArgs.nonce, + signatureArgs.expiry, + signature.v, + signature.r, + signature.s + )); + + // authorizes manager + expect(await comet.isAllowed(alice.address, bob.address)).to.be.true; + + // increments nonce + expect(await comet.userNonce(alice.address)).to.equal(signatureArgs.nonce.add(1)); + + expect(event(tx, 0)).to.be.deep.equal({ + Approval: { + owner: alice.address, + spender: bob.address, + amount: ethers.constants.MaxUint256.toBigInt(), + } + }); + }); }); - }); - it('fails if owner argument is altered', async () => { - expect(await comet.isAllowed(signer.address, manager.address)).to.be.false; - - const invalidOwnerAddress = pauseGuardian.address; - - await expect( - comet.connect(manager).allowBySig( - invalidOwnerAddress, // altered owner - signatureArgs.manager, - signatureArgs.isAllowed, - signatureArgs.nonce, - signatureArgs.expiry, - signature.v, - signature.r, - signature.s - ) - ).to.be.revertedWith("custom error 'BadSignatory()'"); - - // does not authorize - expect(await comet.isAllowed(invalidOwnerAddress, manager.address)).to.be.false; - - // does not alter signer nonce - expect(await comet.userNonce(signer.address)).to.equal(signatureArgs.nonce); + describe('interactions with comet', function () { + this.afterEach(async function () { + await snapshot.restore(); + }); + + it('can supply base token after being authorized', async () => { + await baseToken.allocateTo(alice.address, supplyAmount); + await baseToken.connect(alice).approve(comet.address, supplyAmount); + + expect(await comet.balanceOf(bob.address)).to.equal(0); + await wait(comet.connect(bob).supplyFrom( + alice.address, + bob.address, + baseToken.address, + supplyAmount + )); + expect(await comet.balanceOf(bob.address)).to.be.closeTo(supplyAmount, 1); + }); + + it('can supply collateral after being authorized', async () => { + const collateralAmount = exp(1, 18); + await collaterals.WETH.allocateTo(alice.address, collateralAmount); + await collaterals.WETH.connect(alice).approve(comet.address, collateralAmount); + + expect(await comet.collateralBalanceOf(bob.address, collaterals.WETH.address)).to.equal(0); + await wait(comet.connect(bob).supplyFrom( + alice.address, + bob.address, + collaterals.WETH.address, + collateralAmount + )); + expect(await comet.collateralBalanceOf(bob.address, collaterals.WETH.address)).to.equal(collateralAmount); + }); + + it('can transfer base token after being authorized', async () => { + const balance = await comet.balanceOf(alice.address); + expect(balance).to.be.gt(0); + + expect(await comet.balanceOf(bob.address)).to.equal(0); + await wait(comet.connect(bob).transferFrom( + alice.address, + bob.address, + balance + )); + expect(await comet.balanceOf(bob.address)).to.be.closeTo(balance, 1); + }); + + it('can transfer collateral after being authorized', async () => { + const collateralAmount = exp(1, 18); + await collaterals.WETH.allocateTo(alice.address, collateralAmount); + await collaterals.WETH.connect(alice).approve(comet.address, collateralAmount); + await comet.connect(alice).supply(collaterals.WETH.address, collateralAmount); + + expect(await comet.collateralBalanceOf(bob.address, collaterals.WETH.address)).to.equal(0); + await wait(comet.connect(bob).transferAssetFrom( + alice.address, + bob.address, + collaterals.WETH.address, + collateralAmount + )); + expect(await comet.collateralBalanceOf(bob.address, collaterals.WETH.address)).to.equal(collateralAmount); + }); + + it('can withdraw base token after being authorized', async () => { + const balance = await comet.balanceOf(alice.address); + expect(balance).to.be.gt(0); + + expect(await baseToken.balanceOf(bob.address)).to.equal(0); + await wait(comet.connect(bob).withdrawFrom( + alice.address, + bob.address, + baseToken.address, + balance + )); + expect(await baseToken.balanceOf(bob.address)).to.equal(balance); + }); + + it('can withdraw collateral after being authorized', async () => { + const collateralAmount = exp(1, 18); + await collaterals.WETH.allocateTo(alice.address, collateralAmount); + await collaterals.WETH.connect(alice).approve(comet.address, collateralAmount); + await comet.connect(alice).supply(collaterals.WETH.address, collateralAmount); + + expect(await collaterals.WETH.balanceOf(bob.address)).to.equal(0); + await wait(comet.connect(bob).withdrawFrom( + alice.address, + bob.address, + collaterals.WETH.address, + collateralAmount + )); + expect(await collaterals.WETH.balanceOf(bob.address)).to.equal(collateralAmount); + }); + + it('can borrow after being authorized', async () => { + await comet.connect(alice).withdraw(baseToken.address, supplyAmount); + expect(await comet.balanceOf(alice.address)).to.equal(0); + + const borrowAmount = exp(1000, baseTokenDecimals); + const collateralAmount = exp(1, 18); + await collaterals.WETH.allocateTo(alice.address, collateralAmount); + await collaterals.WETH.connect(alice).approve(comet.address, collateralAmount); + await comet.connect(alice).supply(collaterals.WETH.address, collateralAmount); + + expect(await baseToken.balanceOf(bob.address)).to.equal(0); + await wait(comet.connect(bob).withdrawFrom( + alice.address, + bob.address, + baseToken.address, + borrowAmount + )); + expect(await baseToken.balanceOf(bob.address)).to.equal(borrowAmount); + }); + }); }); - it('fails if manager argument is altered', async () => { - expect(await comet.isAllowed(signer.address, manager.address)).to.be.false; - - const invalidManagerAddress = pauseGuardian.address; - - await expect( - comet.connect(manager).allowBySig( - signatureArgs.owner, - invalidManagerAddress, // altered manager - signatureArgs.isAllowed, - signatureArgs.nonce, - signatureArgs.expiry, - signature.v, - signature.r, - signature.s - ) - ).to.be.revertedWith("custom error 'BadSignatory()'"); - - // does not authorize - expect(await comet.isAllowed(signer.address, invalidManagerAddress)).to.be.false; - - // does not alter signer nonce - expect(await comet.userNonce(signer.address)).to.equal(signatureArgs.nonce); - }); + describe('edge cases', function () { + this.beforeEach(async function () { + await snapshotWithoutAllow.restore(); + }); - it('fails if isAllowed argument is altered', async () => { - expect(await comet.isAllowed(signer.address, manager.address)).to.be.false; - - await expect( - comet.connect(manager).allowBySig( - signatureArgs.owner, - signatureArgs.manager, - !signatureArgs.isAllowed, // altered isAllowed - signatureArgs.nonce, - signatureArgs.expiry, - signature.v, - signature.r, - signature.s - ) - ).to.be.revertedWith("custom error 'BadSignatory()'"); - - // does not authorize - expect(await comet.isAllowed(signer.address, manager.address)).to.be.false; - - // does not alter signer nonce - expect(await comet.userNonce(signer.address)).to.equal(signatureArgs.nonce); - }); + it('fails if owner argument is altered', async () => { + expect(await comet.isAllowed(alice.address, bob.address)).to.be.false; - it('fails if nonce argument is altered', async () => { - expect(await comet.isAllowed(signer.address, manager.address)).to.be.false; - - await expect( - comet.connect(manager).allowBySig( - signatureArgs.owner, - signatureArgs.manager, - signatureArgs.isAllowed, - signatureArgs.nonce.add(1), // altered nonce - signatureArgs.expiry, - signature.v, - signature.r, - signature.s - ) - ).to.be.revertedWith("custom error 'BadSignatory()'"); - - // does not authorize - expect(await comet.isAllowed(signer.address, manager.address)).to.be.false; - - // does not alter signer nonce - expect(await comet.userNonce(signer.address)).to.equal(signatureArgs.nonce); - }); + const invalidOwnerAddress = pauseGuardian.address; - it('fails if expiry argument is altered', async () => { - expect(await comet.isAllowed(signer.address, manager.address)).to.be.false; - - await expect( - comet.connect(manager).allowBySig( - signatureArgs.owner, - signatureArgs.manager, - signatureArgs.isAllowed, - signatureArgs.nonce, - signatureArgs.expiry + 100, // altered expiry - signature.v, - signature.r, - signature.s - ) - ).to.be.revertedWith("custom error 'BadSignatory()'"); - - // does not authorize - expect(await comet.isAllowed(signer.address, manager.address)).to.be.false; - - // does not alter signer nonce - expect(await comet.userNonce(signer.address)).to.equal(signatureArgs.nonce); - }); + await expect( + comet.connect(bob).allowBySig( + invalidOwnerAddress, // altered owner + signatureArgs.manager, + signatureArgs.isAllowed, + signatureArgs.nonce, + signatureArgs.expiry, + signature.v, + signature.r, + signature.s + ) + ).to.be.revertedWith("custom error 'BadSignatory()'"); - it('fails if signature contains invalid nonce', async () => { - const invalidNonce = signatureArgs.nonce.add(1); - const rawSignature = await signer._signTypedData(domain, types, { - ...signatureArgs, - nonce: invalidNonce, + // does not authorize + expect(await comet.isAllowed(invalidOwnerAddress, bob.address)).to.be.false; + + // does not alter signer nonce + expect(await comet.userNonce(alice.address)).to.equal(signatureArgs.nonce); }); - const signatureWithInvalidNonce = ethers.utils.splitSignature(rawSignature); - expect(await comet.isAllowed(signer.address, manager.address)).to.be.false; + it('fails if manager argument is altered', async () => { + expect(await comet.isAllowed(alice.address, bob.address)).to.be.false; - await expect( - comet - .connect(manager) - .allowBySig( + const invalidManagerAddress = pauseGuardian.address; + + await expect( + comet.connect(bob).allowBySig( signatureArgs.owner, - signatureArgs.manager, + invalidManagerAddress, // altered manager signatureArgs.isAllowed, - invalidNonce, + signatureArgs.nonce, signatureArgs.expiry, - signatureWithInvalidNonce.v, - signatureWithInvalidNonce.r, - signatureWithInvalidNonce.s + signature.v, + signature.r, + signature.s ) - ).to.be.revertedWith("custom error 'BadNonce()'"); + ).to.be.revertedWith("custom error 'BadSignatory()'"); - // does not authorize - expect(await comet.isAllowed(signer.address, manager.address)).to.be.false; - // does not update nonce - expect(await comet.userNonce(signer.address)).to.equal(signatureArgs.nonce); - }); + // does not authorize + expect(await comet.isAllowed(alice.address, invalidManagerAddress)).to.be.false; - it('rejects a repeated message', async () => { - // valid call - await comet - .connect(manager) - .allowBySig( - signatureArgs.owner, - signatureArgs.manager, - signatureArgs.isAllowed, - signatureArgs.nonce, - signatureArgs.expiry, - signature.v, - signature.r, - signature.s - ); - - // repeated call - await expect( - comet - .connect(manager) - .allowBySig( + // does not alter signer nonce + expect(await comet.userNonce(alice.address)).to.equal(signatureArgs.nonce); + }); + + it('fails if isAllowed argument is altered', async () => { + expect(await comet.isAllowed(alice.address, bob.address)).to.be.false; + + await expect( + comet.connect(bob).allowBySig( signatureArgs.owner, signatureArgs.manager, - signatureArgs.isAllowed, + !signatureArgs.isAllowed, // altered isAllowed signatureArgs.nonce, signatureArgs.expiry, signature.v, signature.r, signature.s ) - ).to.be.revertedWith("custom error 'BadNonce()'"); - }); + ).to.be.revertedWith("custom error 'BadSignatory()'"); - it('fails if signature expiry has passed', async () => { - const blockNumber = await ethers.provider.getBlockNumber(); - const timestamp = (await ethers.provider.getBlock(blockNumber)).timestamp; - const invalidExpiry = timestamp - 1; + // does not authorize + expect(await comet.isAllowed(alice.address, bob.address)).to.be.false; - const expiredSignatureArgs = { - ...signatureArgs, - expiry: invalidExpiry, - }; - const rawSignature = await signer._signTypedData(domain, types, expiredSignatureArgs); - const expiredSignature = ethers.utils.splitSignature(rawSignature); + // does not alter signer nonce + expect(await comet.userNonce(alice.address)).to.equal(signatureArgs.nonce); + }); - expect(await comet.isAllowed(signer.address, manager.address)).to.be.false; + it('fails if nonce argument is altered', async () => { + expect(await comet.isAllowed(alice.address, bob.address)).to.be.false; - await expect( - comet - .connect(manager) - .allowBySig( - expiredSignatureArgs.owner, - expiredSignatureArgs.manager, - expiredSignatureArgs.isAllowed, - expiredSignatureArgs.nonce, - expiredSignatureArgs.expiry, - expiredSignature.v, - expiredSignature.r, - expiredSignature.s + await expect( + comet.connect(bob).allowBySig( + signatureArgs.owner, + signatureArgs.manager, + signatureArgs.isAllowed, + signatureArgs.nonce.add(1), // altered nonce + signatureArgs.expiry, + signature.v, + signature.r, + signature.s ) - ).to.be.revertedWith("custom error 'SignatureExpired()'"); + ).to.be.revertedWith("custom error 'BadSignatory()'"); - // does not authorize - expect(await comet.isAllowed(signer.address, manager.address)).to.be.false; + // does not authorize + expect(await comet.isAllowed(alice.address, bob.address)).to.be.false; - // does not update nonce - expect(await comet.userNonce(signer.address)).to.equal(signatureArgs.nonce); - }); + // does not alter signer nonce + expect(await comet.userNonce(alice.address)).to.equal(signatureArgs.nonce); + }); - it('fails if v not in {27,28}', async () => { - expect(await comet.isAllowed(signer.address, manager.address)).to.be.false; + it('fails if expiry argument is altered', async () => { + expect(await comet.isAllowed(alice.address, bob.address)).to.be.false; - await expect( - comet - .connect(manager) - .allowBySig( + await expect( + comet.connect(bob).allowBySig( signatureArgs.owner, signatureArgs.manager, signatureArgs.isAllowed, signatureArgs.nonce, - signatureArgs.expiry, - 26, + signatureArgs.expiry + 100, // altered expiry + signature.v, signature.r, signature.s ) - ).to.be.revertedWith("custom error 'InvalidValueV()'"); - - // does not authorize - expect(await comet.isAllowed(signer.address, manager.address)).to.be.false; + ).to.be.revertedWith("custom error 'BadSignatory()'"); - // does not update nonce - expect(await comet.userNonce(signer.address)).to.equal(signatureArgs.nonce); - }); + // does not authorize + expect(await comet.isAllowed(alice.address, bob.address)).to.be.false; - it('fails if s is too high', async () => { - expect(await comet.isAllowed(signer.address, manager.address)).to.be.false; + // does not alter signer nonce + expect(await comet.userNonce(alice.address)).to.equal(signatureArgs.nonce); + }); - // 1 greater than the max value of s - const invalidS = '0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A1'; + it('fails if signature contains invalid nonce', async () => { + const invalidNonce = signatureArgs.nonce.add(1); + const rawSignature = await alice._signTypedData(domain, types, { + ...signatureArgs, + nonce: invalidNonce, + }); + const signatureWithInvalidNonce = ethers.utils.splitSignature(rawSignature); + + expect(await comet.isAllowed(alice.address, bob.address)).to.be.false; + + await expect( + comet + .connect(bob) + .allowBySig( + signatureArgs.owner, + signatureArgs.manager, + signatureArgs.isAllowed, + invalidNonce, + signatureArgs.expiry, + signatureWithInvalidNonce.v, + signatureWithInvalidNonce.r, + signatureWithInvalidNonce.s + ) + ).to.be.revertedWith("custom error 'BadNonce()'"); + + // does not authorize + expect(await comet.isAllowed(alice.address, bob.address)).to.be.false; + // does not update nonce + expect(await comet.userNonce(alice.address)).to.equal(signatureArgs.nonce); + }); - await expect( - comet - .connect(manager) + it('rejects a repeated message', async () => { + // valid call + await comet + .connect(bob) .allowBySig( signatureArgs.owner, signatureArgs.manager, @@ -344,46 +420,145 @@ describe('allowBySig', function () { signatureArgs.expiry, signature.v, signature.r, - invalidS - ) - ).to.be.revertedWith("custom error 'InvalidValueS()'"); - - // does not authorize - expect(await comet.isAllowed(signer.address, manager.address)).to.be.false; - - // does not update nonce - expect(await comet.userNonce(signer.address)).to.equal(signatureArgs.nonce); - }); - - it('fails if owner is zero address', async () => { - expect(await comet.isAllowed(ethers.constants.AddressZero, manager.address)).to.be.false; + signature.s + ); + + // repeated call + await expect( + comet + .connect(bob) + .allowBySig( + signatureArgs.owner, + signatureArgs.manager, + signatureArgs.isAllowed, + signatureArgs.nonce, + signatureArgs.expiry, + signature.v, + signature.r, + signature.s + ) + ).to.be.revertedWith("custom error 'BadNonce()'"); + }); - const blockNumber = await ethers.provider.getBlockNumber(); - const timestamp = (await ethers.provider.getBlock(blockNumber)).timestamp; + it('fails if signature expiry has passed', async () => { + const blockNumber = await ethers.provider.getBlockNumber(); + const timestamp = (await ethers.provider.getBlock(blockNumber)).timestamp; + const invalidExpiry = timestamp - 1; + + const expiredSignatureArgs = { + ...signatureArgs, + expiry: invalidExpiry, + }; + const rawSignature = await alice._signTypedData(domain, types, expiredSignatureArgs); + const expiredSignature = ethers.utils.splitSignature(rawSignature); + + expect(await comet.isAllowed(alice.address, bob.address)).to.be.false; + + await expect( + comet + .connect(bob) + .allowBySig( + expiredSignatureArgs.owner, + expiredSignatureArgs.manager, + expiredSignatureArgs.isAllowed, + expiredSignatureArgs.nonce, + expiredSignatureArgs.expiry, + expiredSignature.v, + expiredSignature.r, + expiredSignature.s + ) + ).to.be.revertedWith("custom error 'SignatureExpired()'"); + + // does not authorize + expect(await comet.isAllowed(alice.address, bob.address)).to.be.false; + + // does not update nonce + expect(await comet.userNonce(alice.address)).to.equal(signatureArgs.nonce); + }); - const invalidSignature = { - v: 27, // valid v - r: '0x0000000000000000000000000000000000000000000000000000000000000000', // invalid r - s: '0x36b99b3646118e24ca7c0c698792ebaf25a4bfa08c1cd6778c335a537b0eb43c', // valid s - }; + it('fails if v not in {27,28}', async () => { + expect(await comet.isAllowed(alice.address, bob.address)).to.be.false; + + await expect( + comet + .connect(bob) + .allowBySig( + signatureArgs.owner, + signatureArgs.manager, + signatureArgs.isAllowed, + signatureArgs.nonce, + signatureArgs.expiry, + 26, + signature.r, + signature.s + ) + ).to.be.revertedWith("custom error 'InvalidValueV()'"); + + // does not authorize + expect(await comet.isAllowed(alice.address, bob.address)).to.be.false; + + // does not update nonce + expect(await comet.userNonce(alice.address)).to.equal(signatureArgs.nonce); + }); - // manager uses invalid signature to force ecrecover to return address(0) - await expect( - comet - .connect(manager) - .allowBySig( - ethers.constants.AddressZero, - manager.address, - true, - await comet.userNonce(ethers.constants.AddressZero), - timestamp + 100, - invalidSignature.v, - invalidSignature.r, - invalidSignature.s, - ) - ).to.be.revertedWith("custom error 'BadSignatory()'"); + it('fails if s is too high', async () => { + expect(await comet.isAllowed(alice.address, bob.address)).to.be.false; + + // 1 greater than the max value of s + const invalidS = '0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A1'; + + await expect( + comet + .connect(bob) + .allowBySig( + signatureArgs.owner, + signatureArgs.manager, + signatureArgs.isAllowed, + signatureArgs.nonce, + signatureArgs.expiry, + signature.v, + signature.r, + invalidS + ) + ).to.be.revertedWith("custom error 'InvalidValueS()'"); + + // does not authorize + expect(await comet.isAllowed(alice.address, bob.address)).to.be.false; + + // does not update nonce + expect(await comet.userNonce(alice.address)).to.equal(signatureArgs.nonce); + }); - // does not authorize manager for address(0) - expect(await comet.isAllowed(ethers.constants.AddressZero, manager.address)).to.be.false; + it('fails if owner is zero address', async () => { + expect(await comet.isAllowed(ethers.constants.AddressZero, bob.address)).to.be.false; + + const blockNumber = await ethers.provider.getBlockNumber(); + const timestamp = (await ethers.provider.getBlock(blockNumber)).timestamp; + + const invalidSignature = { + v: 27, // valid v + r: '0x0000000000000000000000000000000000000000000000000000000000000000', // invalid r + s: '0x36b99b3646118e24ca7c0c698792ebaf25a4bfa08c1cd6778c335a537b0eb43c', // valid s + }; + + // manager uses invalid signature to force ecrecover to return address(0) + await expect( + comet + .connect(bob) + .allowBySig( + ethers.constants.AddressZero, + bob.address, + true, + await comet.userNonce(ethers.constants.AddressZero), + timestamp + 100, + invalidSignature.v, + invalidSignature.r, + invalidSignature.s, + ) + ).to.be.revertedWith("custom error 'BadSignatory()'"); + + // does not authorize manager for address(0) + expect(await comet.isAllowed(ethers.constants.AddressZero, bob.address)).to.be.false; + }); }); }); diff --git a/test/helpers.ts b/test/helpers.ts index 4c6fae1a5..08c15aa74 100644 --- a/test/helpers.ts +++ b/test/helpers.ts @@ -51,8 +51,10 @@ import { takeSnapshot, SnapshotRestorer } from './helpers/snapshot'; // Network helpers export * from './helpers/network-helpers'; +// Math helpers +export * from './helpers/math'; -export { Comet, ethers, expect, hre, takeSnapshot, SnapshotRestorer }; +export { Comet, ethers, expect, hre, takeSnapshot, SnapshotRestorer, BigNumber }; export type Numeric = number | bigint; @@ -157,12 +159,23 @@ export type BulkerInfo = { bulker: BaseBulker; }; +export type UserBasic = { principal: BigNumber, baseTrackingIndex: BigNumber, baseTrackingAccrued: BigNumber, assetsIn: number, _reserved: number }; + + +export const oneDay = 24 * 60 * 60; +export const oneMonth = 30 * oneDay; + export function dfn(x: T | undefined | null, dflt: T): T { return x == undefined ? dflt : x; } export function exp(i: number, d: Numeric = 0, r: Numeric = 6): bigint { - return (BigInt(Math.floor(i * 10 ** Number(r))) * 10n ** BigInt(d)) / 10n ** BigInt(r); + const sign = i < 0 ? -1n : 1n; + const parts = Math.abs(i).toString().split('.'); + const intPart = parts[0]; + const fracPart = (parts[1] || '').padEnd(Number(r), '0').slice(0, Number(r)); + const scaled = BigInt(intPart + fracPart); + return sign * (scaled * 10n ** BigInt(d)) / 10n ** BigInt(r); } export function factor(f: number): bigint { @@ -191,6 +204,62 @@ function toBigInt(f: bigint | BigNumber): bigint { } } +export function mulFactor(n: bigint, factor: bigint):bigint { + return n * factor / factorScale; +} + +export function divPrice(n: bigint, price: bigint, toScale: bigint): bigint { + return n * toScale / price; +} + +const BASE_INDEX_SCALE = 1e15; + +export function presentValueSupply(baseSupplyIndex: bigint | BigNumber, principalValue: bigint | BigNumber): bigint { + const principal = toBigInt(principalValue); + const index = toBigInt(baseSupplyIndex); + return principal * index / BigInt(BASE_INDEX_SCALE); +} + +export function presentValueBorrow(baseBorrowIndex: bigint | BigNumber, principalValue: bigint | BigNumber): bigint { + const principal = toBigInt(principalValue); + const index = toBigInt(baseBorrowIndex); + return principal * index / BigInt(BASE_INDEX_SCALE); +} + +export function presentValue( + principalValue: bigint | BigNumber, + baseSupplyIndex: bigint | BigNumber, + baseBorrowIndex: bigint | BigNumber +): bigint { + const principal = toBigInt(principalValue); + if (principal >= 0n) { + return presentValueSupply(baseSupplyIndex, principal); + } else { + return -presentValueBorrow(baseBorrowIndex, -principal); + } +} + +function principalValueSupply(baseSupplyIndex: bigint, presentValue: bigint): bigint { + return (presentValue * BigInt(BASE_INDEX_SCALE)) / baseSupplyIndex; +} + +function principalValueBorrow(baseBorrowIndex: bigint, presentValue: bigint): bigint { + return (presentValue * BigInt(BASE_INDEX_SCALE) + baseBorrowIndex - 1n) / baseBorrowIndex; +} + +export async function principalValue( + presentValue: bigint | BigNumber, + baseSupplyIndex: bigint | BigNumber, + baseBorrowIndex: bigint | BigNumber +): Promise { + const pv = toBigInt(presentValue); + if (pv >= 0n) { + return principalValueSupply(toBigInt(baseSupplyIndex), pv); + } else { + return -principalValueBorrow(toBigInt(baseBorrowIndex), -pv); + } +} + export function annualize(n: bigint | BigNumber, secondsPerYear = 31536000n): number { return defactor(toBigInt(n) * secondsPerYear); } @@ -227,6 +296,7 @@ export const factorDecimals = 18; export const factorScale = factor(1); export const ONE = factorScale; export const ZERO = factor(0); +export const ZERO_ADDRESS = ethers.constants.AddressZero; export const MAX_ASSETS = 24; export async function getBlock(n?: number, ethers_ = ethers): Promise { diff --git a/test/helpers/math.ts b/test/helpers/math.ts new file mode 100644 index 000000000..e9f849fc2 --- /dev/null +++ b/test/helpers/math.ts @@ -0,0 +1,72 @@ +import { BigNumber } from 'ethers'; + +const factorScale = BigInt(1e18); +const BASE_INDEX_SCALE = BigInt(1e15); + +function toBigInt(f: bigint | BigNumber): bigint { + if (typeof f === 'bigint') { + return f; + } else { + return BigNumber.from(f).toBigInt(); + } +} + +/** + * @notice Multiplies a value by a price and normalizes by a scaling factor. + * @dev Computes (n * price) / fromScale using bigint or BigNumber inputs. + * @param n The value to scale (bigint or BigNumber) + * @param price The price to multiply (bigint or BigNumber) + * @param fromScale The scale to divide by (bigint or BigNumber) + * @return Scaled value as bigint + */ +export function mulPrice(n: bigint | BigNumber, price: bigint | BigNumber, fromScale: bigint | BigNumber): bigint { + return toBigInt(n) * toBigInt(price) / toBigInt(fromScale); +} + +export function mulFactor(n: bigint | BigNumber, factor: bigint | BigNumber): bigint { + return toBigInt(n) * toBigInt(factor) / factorScale; +} + +export function divPrice(n: bigint | BigNumber, price: bigint | BigNumber, toScale: bigint | BigNumber): bigint { + return toBigInt(n) * toBigInt(toScale) / toBigInt(price); +} + +export function presentValueSupply(baseSupplyIndex: bigint | BigNumber, principalValue: bigint | BigNumber): bigint { + return toBigInt(principalValue) * toBigInt(baseSupplyIndex) / BASE_INDEX_SCALE; +} + +export function presentValueBorrow(baseBorrowIndex: bigint | BigNumber, principalValue: bigint | BigNumber): bigint { + return toBigInt(principalValue) * toBigInt(baseBorrowIndex) / BASE_INDEX_SCALE; +} + +export function presentValue( + principalValue: bigint | BigNumber, + baseSupplyIndex: bigint | BigNumber, + baseBorrowIndex: bigint | BigNumber +): bigint { + if (toBigInt(principalValue) >= 0n) { + return presentValueSupply(baseSupplyIndex, principalValue); + } else { + return -presentValueBorrow(baseBorrowIndex, -principalValue); + } +} + +export function principalValueSupply(baseSupplyIndex: bigint | BigNumber, presentValue: bigint | BigNumber): bigint { + return (toBigInt(presentValue) * BASE_INDEX_SCALE) / toBigInt(baseSupplyIndex); +} + +export function principalValueBorrow(baseBorrowIndex: bigint | BigNumber, presentValue: bigint | BigNumber): bigint { + return (toBigInt(presentValue) * BASE_INDEX_SCALE + toBigInt(baseBorrowIndex) - 1n) / toBigInt(baseBorrowIndex); +} + +export function principalValue( + presentValue: bigint | BigNumber, + baseSupplyIndex: bigint | BigNumber, + baseBorrowIndex: bigint | BigNumber +): bigint { + if (toBigInt(presentValue) >= 0n) { + return principalValueSupply(baseSupplyIndex, presentValue); + } else { + return -principalValueBorrow(baseBorrowIndex, -presentValue); + } +}