Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ExternalCallLib which checks revert data for malicious data #2004

Merged
merged 15 commits into from
Nov 15, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions pkg/balancer-js/src/utils/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ const balancerErrorCodes: Record<string, string> = {
'354': 'ADD_OR_REMOVE_BPT',
'355': 'INVALID_CIRCUIT_BREAKER_BOUNDS',
'356': 'CIRCUIT_BREAKER_TRIPPED',
'357': 'MALICIOUS_QUERY_REVERT',
'400': 'REENTRANCY',
'401': 'SENDER_NOT_ALLOWED',
'402': 'PAUSED',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ library Errors {
uint256 internal constant ADD_OR_REMOVE_BPT = 354;
uint256 internal constant INVALID_CIRCUIT_BREAKER_BOUNDS = 355;
uint256 internal constant CIRCUIT_BREAKER_TRIPPED = 356;
uint256 internal constant MALICIOUS_QUERY_REVERT = 357;

// Lib
uint256 internal constant REENTRANCY = 400;
Expand Down
16 changes: 11 additions & 5 deletions pkg/pool-linear/contracts/aave/AaveLinearPool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ pragma solidity ^0.7.0;
pragma experimental ABIEncoderV2;

import "@balancer-labs/v2-interfaces/contracts/pool-linear/IStaticAToken.sol";
import "@balancer-labs/v2-pool-utils/contracts/lib/ExternalCallLib.sol";

import "../LinearPool.sol";

Expand Down Expand Up @@ -69,10 +70,15 @@ contract AaveLinearPool is LinearPool {
// except avoiding storing relevant variables in storage for gas reasons.
// solhint-disable-next-line max-line-length
// see: https://github.com/aave/protocol-v2/blob/ac58fea62bb8afee23f66197e8bce6d79ecda292/contracts/protocol/tokenization/StaticATokenLM.sol#L255-L257
uint256 rate = _lendingPool.getReserveNormalizedIncome(address(getMainToken()));

// This function returns a 18 decimal fixed point number, but `rate` has 27 decimals (i.e. a 'ray' value)
// so we need to convert it.
return rate / 10**9;
try _lendingPool.getReserveNormalizedIncome(address(getMainToken())) returns (uint256 rate) {
// This function returns a 18 decimal fixed point number, but `rate` has 27 decimals (i.e. a 'ray' value)
// so we need to convert it.
return rate / 10**9;
} catch (bytes memory revertData) {
// By maliciously reverting here, Aave (or any other contract in the call stack) could trick the Pool into
// reporting invalid data to the query mechanism for swaps/joins/exits.
// We then check the revert data to ensure this doesn't occur.
ExternalCallLib.bubbleUpNonMaliciousRevert(revertData);
}
}
}
34 changes: 34 additions & 0 deletions pkg/pool-linear/contracts/test/MockAaveLendingPool.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.

// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.

// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

pragma solidity ^0.7.0;

import "@balancer-labs/v2-interfaces/contracts/pool-linear/IStaticAToken.sol";

import "@balancer-labs/v2-pool-utils/contracts/test/MaliciousQueryReverter.sol";

import "@balancer-labs/v2-solidity-utils/contracts/test/TestToken.sol";

contract MockAaveLendingPool is ILendingPool, MaliciousQueryReverter {
uint256 private _rate = 1e27;

function getReserveNormalizedIncome(address) external view override returns (uint256) {
maybeRevertMaliciously();
return _rate;
}

function setReserveNormalizedIncome(uint256 newRate) external {
_rate = newRate;
}
}
18 changes: 6 additions & 12 deletions pkg/pool-linear/contracts/test/MockStaticAToken.sol
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,19 @@ import "@balancer-labs/v2-interfaces/contracts/pool-linear/IStaticAToken.sol";

import "@balancer-labs/v2-solidity-utils/contracts/test/TestToken.sol";

contract MockStaticAToken is TestToken, IStaticAToken, ILendingPool {
uint256 private _rate = 1e27;
contract MockStaticAToken is TestToken, IStaticAToken {
address private immutable _ASSET;
ILendingPool private immutable _lendingPool;

constructor(
string memory name,
string memory symbol,
uint8 decimals,
address underlyingAsset
address underlyingAsset,
ILendingPool lendingPool
) TestToken(name, symbol, decimals) {
_ASSET = underlyingAsset;
_lendingPool = lendingPool;
}

// solhint-disable-next-line func-name-mixedcase
Expand All @@ -38,21 +40,13 @@ contract MockStaticAToken is TestToken, IStaticAToken, ILendingPool {

// solhint-disable-next-line func-name-mixedcase
function LENDING_POOL() external view override returns (ILendingPool) {
return ILendingPool(this);
return _lendingPool;
}

function rate() external pure override returns (uint256) {
revert("Should not call this");
}

function getReserveNormalizedIncome(address) external view override returns (uint256) {
return _rate;
}

function setReserveNormalizedIncome(uint256 newRate) external {
_rate = newRate;
}

function deposit(
address,
uint256,
Expand Down
128 changes: 100 additions & 28 deletions pkg/pool-linear/test/AaveLinearPool.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,17 @@ import Token from '@balancer-labs/v2-helpers/src/models/tokens/Token';
import TokenList from '@balancer-labs/v2-helpers/src/models/tokens/TokenList';
import LinearPool from '@balancer-labs/v2-helpers/src/models/pools/linear/LinearPool';

import { deploy } from '@balancer-labs/v2-helpers/src/contract';
import { deploy, deployedAt } from '@balancer-labs/v2-helpers/src/contract';
import Vault from '@balancer-labs/v2-helpers/src/models/vault/Vault';
import { ZERO_ADDRESS } from '@balancer-labs/v2-helpers/src/constants';
import { MAX_UINT256, ZERO_ADDRESS } from '@balancer-labs/v2-helpers/src/constants';
import { SwapKind } from '@balancer-labs/balancer-js';

enum RevertType {
DoNotRevert,
NonMalicious,
MaliciousSwapQuery,
MaliciousJoinExitQuery,
}

describe('AaveLinearPool', function () {
let vault: Vault;
Expand All @@ -29,12 +37,15 @@ describe('AaveLinearPool', function () {
});

sharedBeforeEach('deploy tokens', async () => {
mockLendingPool = await deploy('MockAaveLendingPool');

mainToken = await Token.create('DAI');
const wrappedTokenInstance = await deploy('MockStaticAToken', { args: ['cDAI', 'cDAI', 18, mainToken.address] });
const wrappedTokenInstance = await deploy('MockStaticAToken', {
args: ['cDAI', 'cDAI', 18, mainToken.address, mockLendingPool.address],
});
wrappedToken = await Token.deployedAt(wrappedTokenInstance.address);

tokens = new TokenList([mainToken, wrappedToken]).sort();
mockLendingPool = wrappedTokenInstance;

await tokens.mint({ to: [lp, trader], amount: fp(100) });
});
Expand Down Expand Up @@ -64,13 +75,32 @@ describe('AaveLinearPool', function () {
pool = await LinearPool.deployedAt(event.args.pool);
});

describe('constructor', () => {
it('reverts if the mainToken is not the ASSET of the wrappedToken', async () => {
const otherToken = await Token.create('USDC');

await expect(
poolFactory.create(
'Balancer Pool Token',
'BPT',
otherToken.address,
wrappedToken.address,
bn(0),
POOL_SWAP_FEE_PERCENTAGE,
owner.address
)
).to.be.revertedWith('TOKENS_MISMATCH');
});
});

describe('asset managers', () => {
it('sets the same asset manager for main and wrapped token', async () => {
const poolId = await pool.getPoolId();

const { assetManager: firstAssetManager } = await vault.getPoolTokenInfo(poolId, tokens.first);
const { assetManager: secondAssetManager } = await vault.getPoolTokenInfo(poolId, tokens.second);

expect(firstAssetManager).to.not.equal(ZERO_ADDRESS);
expect(firstAssetManager).to.equal(secondAssetManager);
});

Expand All @@ -82,33 +112,75 @@ describe('AaveLinearPool', function () {
});

describe('getWrappedTokenRate', () => {
it('returns the expected value', async () => {
// Reserve's normalised income is stored with 27 decimals (i.e. a 'ray' value)
// 1e27 implies a 1:1 exchange rate between main and wrapped token
await mockLendingPool.setReserveNormalizedIncome(bn(1e27));
expect(await pool.getWrappedTokenRate()).to.be.eq(fp(1));

// We now double the reserve's normalised income to change the exchange rate to 2:1
await mockLendingPool.setReserveNormalizedIncome(bn(2e27));
expect(await pool.getWrappedTokenRate()).to.be.eq(fp(2));
context('under normal operation', () => {
it('returns the expected value', async () => {
// Reserve's normalised income is stored with 27 decimals (i.e. a 'ray' value)
// 1e27 implies a 1:1 exchange rate between main and wrapped token
await mockLendingPool.setReserveNormalizedIncome(bn(1e27));
expect(await pool.getWrappedTokenRate()).to.be.eq(fp(1));

// We now double the reserve's normalised income to change the exchange rate to 2:1
await mockLendingPool.setReserveNormalizedIncome(bn(2e27));
expect(await pool.getWrappedTokenRate()).to.be.eq(fp(2));
});
});
});

describe('constructor', () => {
it('reverts if the mainToken is not the ASSET of the wrappedToken', async () => {
const otherToken = await Token.create('USDC');
context('when Aave reverts maliciously to impersonate a swap query', () => {
sharedBeforeEach('make Aave lending pool start reverting', async () => {
await mockLendingPool.setRevertType(RevertType.MaliciousSwapQuery);
});

await expect(
poolFactory.create(
'Balancer Pool Token',
'BPT',
otherToken.address,
wrappedToken.address,
bn(0),
POOL_SWAP_FEE_PERCENTAGE,
owner.address
)
).to.be.revertedWith('TOKENS_MISMATCH');
it('reverts with MALICIOUS_QUERY_REVERT', async () => {
await expect(pool.getWrappedTokenRate()).to.be.revertedWith('MALICIOUS_QUERY_REVERT');
});
});

context('when Aave reverts maliciously to impersonate a join/exit query', () => {
sharedBeforeEach('make Aave lending pool start reverting', async () => {
await mockLendingPool.setRevertType(RevertType.MaliciousJoinExitQuery);
});

it('reverts with MALICIOUS_QUERY_REVERT', async () => {
await expect(pool.getWrappedTokenRate()).to.be.revertedWith('MALICIOUS_QUERY_REVERT');
});
});
});

describe('rebalancing', () => {
context('when Aave reverts maliciously to impersonate a swap query', () => {
let rebalancer: Contract;
sharedBeforeEach('provide initial liquidity to pool', async () => {
const poolId = await pool.getPoolId();

await tokens.approve({ to: vault, amount: fp(100), from: lp });
await vault.instance.connect(lp).swap(
{
poolId,
kind: SwapKind.GivenIn,
assetIn: mainToken.address,
assetOut: pool.address,
amount: fp(10),
userData: '0x',
},
{ sender: lp.address, fromInternalBalance: false, recipient: lp.address, toInternalBalance: false },
0,
MAX_UINT256
);
});

sharedBeforeEach('deploy and initialize pool', async () => {
const poolId = await pool.getPoolId();
const { assetManager } = await vault.getPoolTokenInfo(poolId, tokens.first);
rebalancer = await deployedAt('AaveLinearPoolRebalancer', assetManager);
});

sharedBeforeEach('make Aave lending pool start reverting', async () => {
await mockLendingPool.setRevertType(RevertType.MaliciousSwapQuery);
});

it('reverts with MALICIOUS_QUERY_REVERT', async () => {
await expect(rebalancer.rebalance(trader.address)).to.be.revertedWith('MALICIOUS_QUERY_REVERT');
});
});
});
});
6 changes: 5 additions & 1 deletion pkg/pool-linear/test/AaveLinearPoolFactory.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,12 @@ describe('AaveLinearPoolFactory', function () {
});
creationTime = await currentTimestamp();

const mockLendingPool = await deploy('MockAaveLendingPool');

const mainToken = await Token.create('DAI');
const wrappedTokenInstance = await deploy('MockStaticAToken', { args: ['cDAI', 'cDAI', 18, mainToken.address] });
const wrappedTokenInstance = await deploy('MockStaticAToken', {
args: ['cDAI', 'cDAI', 18, mainToken.address, mockLendingPool.address],
});
const wrappedToken = await Token.deployedAt(wrappedTokenInstance.address);

tokens = new TokenList([mainToken, wrappedToken]).sort();
Expand Down
55 changes: 55 additions & 0 deletions pkg/pool-utils/contracts/lib/ExternalCallLib.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.

// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.

// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

pragma solidity ^0.7.0;

import "@balancer-labs/v2-interfaces/contracts/solidity-utils/helpers/BalancerErrors.sol";

library ExternalCallLib {
function bubbleUpNonMaliciousRevert(bytes memory errorData) internal pure {
uint256 errorLength = errorData.length;

// solhint-disable-next-line no-inline-assembly
assembly {
// If the first 4 bytes match the selector for one of the error signatures used by `BasePool._queryAction`
// or `Vault.queryBatchSwap` then this error is attempting to impersonate the query mechanism used by these
// contracts in order to inject bogus data. This can result in loss of funds if the return value is then
// used in a later calculation.
//
// We then want to reject the following error signatures:
// - `QueryError(uint256,uint256[])` (used by `BasePool._queryAction`)
// - `QueryError(int256[])` (used by `Vault.queryBatchSwap`)

// We only bubble up the revert reason if it doesn't match the any of the selectors for these error
// sigatures, otherwise we revert with a new error message flagging that the revert was malicious.
let error := and(
mload(add(errorData, 0x20)),
0xffffffff00000000000000000000000000000000000000000000000000000000
)
if iszero(
or(
// BasePool._queryAction
eq(error, 0x43adbafb00000000000000000000000000000000000000000000000000000000),
// Vault.queryBatchSwap
eq(error, 0xfa61cc1200000000000000000000000000000000000000000000000000000000)
)
) {
revert(add(errorData, 0x20), errorLength)
}
}

// We expect the assembly block to revert for all non-malicious errors.
_revert(Errors.MALICIOUS_QUERY_REVERT);
}
}
Loading