diff --git a/packages/relayer/certora/conf/relayer.conf b/packages/relayer/certora/conf/relayer.conf index ab2c0e09..ad22e8bf 100644 --- a/packages/relayer/certora/conf/relayer.conf +++ b/packages/relayer/certora/conf/relayer.conf @@ -16,6 +16,7 @@ "optimistic_loop": true, "packages": [ "@mimic-fi/v3-authorizer=../../node_modules/@mimic-fi/v3-authorizer", + "@mimic-fi/v3-helpers=../../node_modules/@mimic-fi/v3-helpers", "@mimic-fi/v3-smart-vault=../../node_modules/@mimic-fi/v3-smart-vault", "@mimic-fi/v3-tasks=../../node_modules/@mimic-fi/v3-tasks", "@openzeppelin=../../node_modules/@openzeppelin" diff --git a/packages/relayer/certora/helpers/Helpers.sol b/packages/relayer/certora/helpers/Helpers.sol index 11b3a17d..32f29d7d 100644 --- a/packages/relayer/certora/helpers/Helpers.sol +++ b/packages/relayer/certora/helpers/Helpers.sol @@ -2,12 +2,18 @@ pragma solidity ^0.8.0; +import '@mimic-fi/v3-helpers/contracts/utils/Denominations.sol'; +import '@mimic-fi/v3-helpers/contracts/utils/ERC20Helpers.sol'; import '@mimic-fi/v3-smart-vault/contracts/interfaces/ISmartVault.sol'; import '@mimic-fi/v3-tasks/contracts/interfaces/ITask.sol'; contract Helpers { - function balanceOf(address account) external view returns (uint256) { - return address(account).balance; + function NATIVE_TOKEN() external pure returns (address) { + return Denominations.NATIVE_TOKEN; + } + + function balanceOf(address token, address account) external view returns (uint256) { + return ERC20Helpers.balanceOf(token, account); } function areValidTasks(address[] memory tasks) external view returns (bool) { diff --git a/packages/relayer/certora/specs/Relayer.spec b/packages/relayer/certora/specs/Relayer.spec index d84dcd22..f9cc732a 100644 --- a/packages/relayer/certora/specs/Relayer.spec +++ b/packages/relayer/certora/specs/Relayer.spec @@ -9,7 +9,8 @@ using Depositor as Depositor; methods { // Helpers - function helpers.balanceOf(address) external returns (uint256) envfree; + function helpers.NATIVE_TOKEN() external returns (address) envfree => ALWAYS(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE); + function helpers.balanceOf(address,address) external returns (uint256) envfree; function helpers.areValidTasks(address[]) external returns (bool) envfree; // Relayer @@ -20,11 +21,13 @@ methods { function getSmartVaultCollector(address) external returns (address) envfree; function getSmartVaultMaxQuota(address) external returns (uint256) envfree; function getSmartVaultUsedQuota(address) external returns (uint256) envfree; + function getApplicableCollector(address) external returns (address) envfree; function payTransactionGasToRelayer(address,uint256) external envfree; // Wildcard entries function _.smartVault() external => PER_CALLEE_CONSTANT; function _.hasPermissions(address) external => PER_CALLEE_CONSTANT; + function _.balanceOf(address) external => DISPATCHER(true); } @@ -96,7 +99,7 @@ hook Sstore currentContract.getSmartVaultBalance[KEY address smartVault] uint256 /************************************************************/ invariant contractBalanceIsSumOfBalances() - ghostSumOfSmartVaultBalances <= helpers.balanceOf(currentContract) + ghostSumOfSmartVaultBalances <= helpers.balanceOf(helpers.NATIVE_TOKEN(), currentContract) filtered { f -> f.selector != SIMULATE() } { preserved with (env e) { require e.msg.sender != currentContract; } } @@ -265,20 +268,30 @@ rule depositValidAmount(env e, address smartVault, uint256 amount) } rule depositProperBalances(env e, address smartVault, uint256 amount) - good_description "After calling `deposit` the smart vault balance is increased at most by `amount`" + good_description "After calling `deposit` smart vault, relayer and collector balances are increased properly" { require e.msg.sender != currentContract; + address collector = getApplicableCollector(smartVault); + require collector != e.msg.sender; + require collector != currentContract; + uint256 initialSmartVaultBalance = getSmartVaultBalance(smartVault); - uint256 initialRelayerBalance = helpers.balanceOf(currentContract); + uint256 initialRelayerBalance = helpers.balanceOf(helpers.NATIVE_TOKEN(), currentContract); + uint256 initCollectorBalance = helpers.balanceOf(helpers.NATIVE_TOKEN(), collector); + uint256 initialUsedQuota = getSmartVaultUsedQuota(smartVault); deposit(e, smartVault, amount); uint256 currentSmartVaultBalance = getSmartVaultBalance(smartVault); - uint256 currentRelayerBalance = helpers.balanceOf(currentContract); + uint256 currentRelayerBalance = helpers.balanceOf(helpers.NATIVE_TOKEN(), currentContract); + uint256 currentCollectorBalance = helpers.balanceOf(helpers.NATIVE_TOKEN(), collector); + uint256 currentUsedQuota = getSmartVaultUsedQuota(smartVault); + mathint quotaPaid = initialUsedQuota - currentUsedQuota; - assert to_mathint(currentSmartVaultBalance) <= initialSmartVaultBalance + amount; - assert to_mathint(currentRelayerBalance) == initialRelayerBalance + amount; + assert to_mathint(currentSmartVaultBalance) == initialSmartVaultBalance + amount - quotaPaid; + assert to_mathint(currentRelayerBalance) == initialRelayerBalance + amount - quotaPaid; + assert to_mathint(currentCollectorBalance) == initCollectorBalance + quotaPaid; } rule payQuotaProperBalances(env e, address smartVault, uint256 amount) @@ -306,12 +319,12 @@ rule withdrawProperBalances(env e, uint256 amount) good_description "After calling `withdraw` the smart vault balance is decreased by `amount`" { uint256 initialSmartVaultBalance = getSmartVaultBalance(e.msg.sender); - uint256 initialRelayerBalance = helpers.balanceOf(currentContract); + uint256 initialRelayerBalance = helpers.balanceOf(helpers.NATIVE_TOKEN(), currentContract); withdraw(e, amount); uint256 currentSmartVaultBalance = getSmartVaultBalance(e.msg.sender); - uint256 currentRelayerBalance = helpers.balanceOf(currentContract); + uint256 currentRelayerBalance = helpers.balanceOf(helpers.NATIVE_TOKEN(), currentContract); assert to_mathint(currentSmartVaultBalance) == initialSmartVaultBalance - amount; assert to_mathint(currentRelayerBalance) == initialRelayerBalance - amount; @@ -326,10 +339,10 @@ rule withdrawIntegrity(env e, uint256 amount) uint256 smartVaultBalance = getSmartVaultBalance(e.msg.sender); require amount <= smartVaultBalance; - uint256 relayerBalance = helpers.balanceOf(currentContract); + uint256 relayerBalance = helpers.balanceOf(helpers.NATIVE_TOKEN(), currentContract); require relayerBalance >= amount; - uint256 senderBalance = helpers.balanceOf(e.msg.sender); + uint256 senderBalance = helpers.balanceOf(helpers.NATIVE_TOKEN(), e.msg.sender); require senderBalance + amount <= MAX_UINT256(); withdraw@withrevert(e, amount); @@ -412,3 +425,16 @@ rule simulateAlwaysReverts(env e, address[] tasks, bytes[] data, bool continueIf assert lastReverted; } + +rule notLockedFunds(env e, method f, calldataarg args, address token) + filtered { f -> f.selector == EXECUTE() || f.selector == DEPOSIT() || f.selector == WITHDRAW() } + good_description "There is always a way to withdraw tokens from the contract" +{ + uint256 initialBalance = helpers.balanceOf(token, currentContract); + require initialBalance > 0; + + f(e, args); + + uint256 currentBalance = helpers.balanceOf(token, currentContract); + satisfy currentBalance == 0; +} diff --git a/packages/relayer/contracts/Relayer.sol b/packages/relayer/contracts/Relayer.sol index 480e27fb..a8ed43cc 100644 --- a/packages/relayer/contracts/Relayer.sol +++ b/packages/relayer/contracts/Relayer.sol @@ -298,6 +298,8 @@ contract Relayer is IRelayer, Ownable { quotaPaid = toDeposit; } + (bool paySuccess, ) = getApplicableCollector(smartVault).call{ value: quotaPaid }(''); + if (!paySuccess) revert RelayerQuotaPaymentFailed(smartVault, quotaPaid); emit QuotaPaid(smartVault, quotaPaid); } } diff --git a/packages/relayer/contracts/interfaces/IRelayer.sol b/packages/relayer/contracts/interfaces/IRelayer.sol index 6edbe639..839c0a69 100644 --- a/packages/relayer/contracts/interfaces/IRelayer.sol +++ b/packages/relayer/contracts/interfaces/IRelayer.sol @@ -78,6 +78,11 @@ interface IRelayer { */ error RelayerPaymentFailed(address smartVault, uint256 amount, uint256 quota); + /** + * @dev It failed to send owed quota to the smart vault's collector + */ + error RelayerQuotaPaymentFailed(address smartVault, uint256 quota); + /** * @dev The smart vault balance is lower than the amount to withdraw */ diff --git a/packages/relayer/test/Relayer.test.ts b/packages/relayer/test/Relayer.test.ts index 9662dc4e..61e9f14b 100644 --- a/packages/relayer/test/Relayer.test.ts +++ b/packages/relayer/test/Relayer.test.ts @@ -218,18 +218,20 @@ describe('Relayer', () => { }) describe('deposit', () => { - let smartVault: SignerWithAddress - - const amount = fp(0.1) + let authorizer: Contract, smartVault: Contract, smartVaultOwner: SignerWithAddress - beforeEach('load smart vault', async () => { - smartVault = await getSigner() + beforeEach('deploy smart vault', async () => { + smartVaultOwner = await getSigner() + // eslint-disable-next-line prettier/prettier + ;({ authorizer, smartVault } = await deployEnvironment(smartVaultOwner)) }) - context('when the given value is correct', () => { - const value = amount + context('when the used quota is zero', () => { + const amount = fp(0.1) + + context('when the given value is correct', () => { + const value = amount - context('when the used quota is zero', () => { it('deposits the amount correctly', async () => { const previousSmartVaultBalance = await relayer.getSmartVaultBalance(smartVault.address) @@ -255,111 +257,114 @@ describe('Relayer', () => { }) }) - context('when the used quota is not zero', () => { - let authorizer: Contract, smartVault: Contract, smartVaultOwner: SignerWithAddress - let usedQuota: BigNumber, currentQuota: BigNumber, paidQuota: BigNumber - let amount: BigNumber, value: BigNumber, toDeposit: BigNumber + context('when the given value is not correct', () => { + const value = amount.sub(1) - beforeEach('deploy smart vault', async () => { - smartVaultOwner = await getSigner() - // eslint-disable-next-line prettier/prettier - ;({ authorizer, smartVault } = await deployEnvironment(smartVaultOwner)) + it('reverts', async () => { + await expect(relayer.deposit(smartVault.address, amount, { value })).to.revertedWith( + 'RelayerValueDoesNotMatchAmount' + ) }) + }) + }) - beforeEach('set maximum quota', async () => { - const maxQuota = fp(10000) - await relayer.connect(owner).setSmartVaultMaxQuota(smartVault.address, maxQuota) - }) + context('when the used quota is not zero', () => { + let amount: BigNumber + let expectedCurrentQuota: BigNumber, expectedPaidQuota: BigNumber, expectedDepositAmount: BigNumber + + beforeEach('set maximum quota', async () => { + const maxQuota = fp(10000) + await relayer.connect(owner).setSmartVaultMaxQuota(smartVault.address, maxQuota) + }) - beforeEach('use some quota', async () => { - const task = await deploy('TaskMock', [smartVault.address]) + beforeEach('use some quota', async () => { + const task = await deploy('TaskMock', [smartVault.address]) - await authorizer.connect(smartVaultOwner).authorize(task.address, smartVault.address, '0xaabbccdd', []) - await relayer.deposit(ZERO_ADDRESS, fp(1.5), { value: fp(1.5) }) + await authorizer.connect(smartVaultOwner).authorize(task.address, smartVault.address, '0xaabbccdd', []) + await relayer.deposit(ZERO_ADDRESS, fp(1.5), { value: fp(1.5) }) - const data = task.interface.encodeFunctionData('succeed') - const tx = await relayer.connect(executor).execute([task.address], [data], false) - await tx.wait() + const data = task.interface.encodeFunctionData('succeed') + const tx = await relayer.connect(executor).execute([task.address], [data], false) + await tx.wait() + }) + + const itDepositsBalanceProperly = () => { + it('has no balance', async () => { + expect(await relayer.getSmartVaultBalance(smartVault.address)).to.be.equal(0) }) - const itBehavesLikeDeposit = () => { - it('has no balance', async () => { - expect(await relayer.getSmartVaultBalance(smartVault.address)).to.be.equal(0) - }) + it('deposits the amount correctly', async () => { + const previousSmartVaultBalance = fp(0) - it('deposits the amount correctly', async () => { - const previousSmartVaultBalance = fp(0) + await relayer.deposit(smartVault.address, amount, { value: amount }) - await relayer.deposit(smartVault.address, amount, { value }) + const currentSmartVaultBalance = await relayer.getSmartVaultBalance(smartVault.address) + expect(currentSmartVaultBalance).to.be.equal(previousSmartVaultBalance.add(expectedDepositAmount)) + }) - const currentSmartVaultBalance = await relayer.getSmartVaultBalance(smartVault.address) - expect(currentSmartVaultBalance).to.be.equal(previousSmartVaultBalance.add(toDeposit)) - }) + it('increments the relayer balance properly', async () => { + const previousRelayerBalance = await ethers.provider.getBalance(relayer.address) - it('increments the relayer balance properly', async () => { - const previousRelayerBalance = await ethers.provider.getBalance(relayer.address) + await relayer.deposit(smartVault.address, amount, { value: amount }) - await relayer.deposit(smartVault.address, amount, { value }) + const currentRelayerBalance = await ethers.provider.getBalance(relayer.address) + expect(currentRelayerBalance).to.be.equal(previousRelayerBalance.add(expectedDepositAmount)) + }) - const currentRelayerBalance = await ethers.provider.getBalance(relayer.address) - expect(currentRelayerBalance).to.be.equal(previousRelayerBalance.add(amount)) - }) + it('pays the corresponding quota to the fee collector', async () => { + const collector = await relayer.getApplicableCollector(smartVault.address) + const previousCollectorBalance = await ethers.provider.getBalance(collector) - it('emits an event', async () => { - const tx = await relayer.deposit(smartVault.address, amount, { value }) + await relayer.deposit(smartVault.address, amount, { value: amount }) - await assertEvent(tx, 'Deposited', { smartVault, amount: toDeposit }) - }) + const currentCollectorBalance = await ethers.provider.getBalance(collector) + expect(currentCollectorBalance).to.be.equal(previousCollectorBalance.add(expectedPaidQuota)) + }) - it('decrements the used quota properly', async () => { - await relayer.deposit(smartVault.address, amount, { value }) + it('emits an event', async () => { + const tx = await relayer.deposit(smartVault.address, amount, { value: amount }) - const currentUsedQuota = await relayer.getSmartVaultUsedQuota(smartVault.address) - expect(currentUsedQuota).to.be.equal(currentQuota) - }) + await assertEvent(tx, 'Deposited', { smartVault, amount: expectedDepositAmount }) + }) - it('emits an event', async () => { - const tx = await relayer.deposit(smartVault.address, amount, { value }) + it('decrements the used quota properly', async () => { + await relayer.deposit(smartVault.address, amount, { value: amount }) - await assertEvent(tx, 'QuotaPaid', { smartVault, amount: paidQuota }) - }) - } - - context('when the used quota is lower than the amount', () => { - beforeEach('set data', async () => { - usedQuota = await relayer.getSmartVaultUsedQuota(smartVault.address) - amount = usedQuota.mul(2) - value = amount - toDeposit = amount.sub(usedQuota) - currentQuota = fp(0) - paidQuota = usedQuota - }) + const currentUsedQuota = await relayer.getSmartVaultUsedQuota(smartVault.address) + expect(currentUsedQuota).to.be.equal(expectedCurrentQuota) + }) + + it('emits an event', async () => { + const tx = await relayer.deposit(smartVault.address, amount, { value: amount }) - itBehavesLikeDeposit() + await assertEvent(tx, 'QuotaPaid', { smartVault, amount: expectedPaidQuota }) }) + } - context('when the used quota is greater than or equal to the amount', () => { - beforeEach('set data', async () => { - usedQuota = await relayer.getSmartVaultUsedQuota(smartVault.address) - amount = usedQuota.div(2) - value = amount - toDeposit = fp(0) - currentQuota = usedQuota.sub(amount) - paidQuota = amount - }) + context('when the used quota is lower than the amount', () => { + beforeEach('set data', async () => { + const usedQuota = await relayer.getSmartVaultUsedQuota(smartVault.address) + amount = usedQuota.mul(2) - itBehavesLikeDeposit() + expectedDepositAmount = amount.sub(usedQuota) + expectedCurrentQuota = fp(0) + expectedPaidQuota = usedQuota }) + + itDepositsBalanceProperly() }) - }) - context('when the given value is not correct', () => { - const value = amount.sub(1) + context('when the used quota is greater than or equal to the amount', () => { + beforeEach('set data', async () => { + const usedQuota = await relayer.getSmartVaultUsedQuota(smartVault.address) + amount = usedQuota.div(2) - it('reverts', async () => { - await expect(relayer.deposit(smartVault.address, amount, { value })).to.revertedWith( - 'RelayerValueDoesNotMatchAmount' - ) + expectedDepositAmount = fp(0) + expectedCurrentQuota = usedQuota.sub(amount) + expectedPaidQuota = amount + }) + + itDepositsBalanceProperly() }) }) }) @@ -968,6 +973,7 @@ describe('Relayer', () => { context('when the amount is zero', () => { const amount = 0 + it('reverts', async () => { await expect(relayer.rescueFunds(token.address, recipient.address, amount)).to.be.revertedWith( 'RelayerAmountZero' @@ -977,9 +983,10 @@ describe('Relayer', () => { }) context('when the recipient is the zero address', () => { - const recipientAddr = ZERO_ADDRESS + const recipientAddress = ZERO_ADDRESS + it('reverts', async () => { - await expect(relayer.rescueFunds(token.address, recipientAddr, amount)).to.be.revertedWith( + await expect(relayer.rescueFunds(token.address, recipientAddress, amount)).to.be.revertedWith( 'RelayerRecipientZero' ) }) @@ -987,9 +994,12 @@ describe('Relayer', () => { }) context('when the token is the zero address', () => { - const tokenAddr = ZERO_ADDRESS + const tokenAddress = ZERO_ADDRESS + it('reverts', async () => { - await expect(relayer.rescueFunds(tokenAddr, recipient.address, amount)).to.be.revertedWith('RelayerTokenZero') + await expect(relayer.rescueFunds(tokenAddress, recipient.address, amount)).to.be.revertedWith( + 'RelayerTokenZero' + ) }) }) })