<100 subscribers

The developers of the last lending pool are saying that they've learned the lesson. And just released a new version!
Now they're using a Uniswap v2 exchange as a price oracle, along with the recommended utility libraries. That should be enough.
You start with 20 ETH and 10000 DVT tokens in balance. The new lending pool has a million DVT tokens in balance. You know what to do ;
The CAMM liquidity pool exchange model is based on the Uniswap Protocol v2. The UniswapV2Pair performs the role of an AMM and tracks the balance of the pool tokens (sets up the Oracle Price Data Feed). The UniswapV2Factory holds the general bytecode that supplies power to the factory pair and its main task is to create only one smart contract per unique token pair. The UniswapV2Router, a library used by the router, fully supports all basic requirements of the front-end that provides trading and liquidity management functions, particularly supporting multi-pair trades (e.g. x to y to z) by default and treating ETH as a first-class citizen, and providing meta-trades for liquidity removal.
summary
The attacker is provided with 20 ether.
The DVT and WETH token contracts are deployed.
The UniswapRouter Contract is deployed and creates a DVT-WETH pair with liquidity of 100 DVT and 10 WETH.
The PuppetV2Pool is deployed.
The attacker is provided with 10,000 DVT.
// (uniswap)liquidity : 100DVT, 10ETH
uint256 internal constant UNISWAP_INITIAL_TOKEN_RESERVE = 100e18;
uint256 internal constant UNISWAP_INITIAL_WETH_RESERVE = 10 ether;
// (Pool) : 1,000,000DVT
uint256 internal constant POOL_INITIAL_TOKEN_BALANCE = 1_000_000e18;
uint256 internal constant DEADLINE = 10_000_000;
// (Attacker) : 10,000DVT, 20ETH
uint256 internal constant ATTACKER_INITIAL_TOKEN_BALANCE = 10_000e18;
uint256 internal constant ATTACKER_INITIAL_ETH_BALANCE = 20 ether;
constructor() {
string[] memory labels = new stringUnsupported embed;
labels[0] = "attacker";
preSetup(1, ATTACKER_INITIAL_ETH_BALANCE, labels);
}
function setUp() public override {
super.setUp();
dvt = new DamnValuableToken();
vm.label(address(dvt), "DVT");
weth = new WETH9();
vm.label(address(weth), "WETH");
// (Deploy) Uniswap Factory,Router
uniswapV2Factory = IUniswapV2Factory(
deployCode(
"artifacts/build-uniswap-v2/UniswapV2Factory.json",
abi.encode(address(0))
)
);
uniswapV2Router = IUniswapV2Router02(
deployCode(
"artifacts/build-uniswap-v2/UniswapV2Router02.json",
abi.encode(address(uniswapV2Factory), address(weth))
)
);
The necessary setup process is carried out. The compiled uniswap (factory, Router, pair) and ERC 20 (DVT, WETH) contracts are deployed and the artifacts are integrated.
// (Create) Uniswap pair -- add liquidity
dvt.approve(address(uniswapV2Router), UNISWAP_INITIAL_TOKEN_RESERVE);
uniswapV2Router.addLiquidityETH{value: UNISWAP_INITIAL_WETH_RESERVE}(
address(dvt),
UNISWAP_INITIAL_TOKEN_RESERVE, // amountTokenDesired
0, // amountTokenMin
0, // amountETHMin
address(this), // to
DEADLINE // deadline
);
// getPair ref=> uniswap pair
uniswapV2Pair = IUniswapV2Pair(
uniswapV2Factory.getPair(address(dvt), address(weth))
);
Based on the DVT token contract, the uniswapRouter contract address is set up with initial liquidity by performing an approve operation with a value of UNISWAP_INITIAL_TOKEN_RESERVE: 100 ether.
UniswapV2Router addLiquidityETH
// (Create) Uniswap pair -- add liquidity
dvt.approve(address(uniswapV2Router), UNISWAP_INITIAL_TOKEN_RESERVE);
uniswapV2Router.addLiquidityETH{value: UNISWAP_INITIAL_WETH_RESERVE}(
address(dvt),
UNISWAP_INITIAL_TOKEN_RESERVE, // amountTokenDesired
0, // amountTokenMin
0, // amountETHMin
address(this), // to
DEADLINE // deadline
);
Adds liquidity to an ERC20 ↔ WETH pool with ETH.
To cover all possible scenarios, msg.sender should have already given the router an allowance of at least amountTokenDesired on token.
Always adds assets at the ideal ratio, according to the price when the transaction is executed.
msg.value is treated as a amountETHDesired
Letfover ETH, if any, is returned to msg.sender
if a pool for the passed token and WETH does not exists, one is created automatically, and exactly amountTokenDesired / msg.value tokens are added.
Arguments
address Token
A Pool Token
uint amountTokenDesired
The amount of token to add as liquidity if the WETH/token price is msg.value/amountTokenDesired (token depreciates).
uint amountETHDesired <msg.value>
The amount of ETH to add as liquidity if the token/WETH price is amountTokenDesired/msg.value (WETH depreciates)
uint amountTokenMin
Bounds the extent to which the WETH/token price can go up before the transaction reverts. Must be amountTokenDesired
uint amountETHMin
Bounds the extent to which the token/WETH price can go up before the transaction reverts. Must be msg.value
address to
Recipient of the liquidity tokens
uint deadline
Unix timestamp after which the transaction will revert.
Based on the DVT token contract address, 100 ether is assigned to the amountTokenDesired and Wrapper operation is performed, keeping the current msg.value value at 10 ether under the given conditions.
// getPair ref=> uniswap pair
uniswapV2Pair = IUniswapV2Pair(
uniswapV2Factory.getPair(address(dvt), address(weth))
);
assertGt(uniswapV2Pair.balanceOf(address(this)), 0);
// Deploy the lending pool
puppetV2Pool = new PuppetV2Pool(
address(weth),
address(dvt),
address(uniswapV2Pair),
address(uniswapV2Factory)
);
// Setup initial token balances of pool and attacker account
dvt.transfer(attacker, ATTACKER_INITIAL_TOKEN_BALANCE);
dvt.transfer(address(puppetV2Pool), POOL_INITIAL_TOKEN_BALANCE);
// Ensure correct setup of pool.
assertEq(
puppetV2Pool.calculateDepositOfWETHRequired(1 ether),
0.3 ether
);
assertEq(
puppetV2Pool.calculateDepositOfWETHRequired(
POOL_INITIAL_TOKEN_BALANCE
),
300_000 ether
);
The uniswapV2Exchange contract's getPair function is used to obtain the DVT and WETH token pair contract address, which will be used as an instance.
The uniswapExchange's balance value is checked to see if it is 0.
The attacker account is allocated 10000 ether, and the lending pool contract is allocated 1000000 ether.
PuppetV2Pool.sol

State variables are declared for allocating dependent contract instances.
The deposits mapping data is used to create a mapping structure for handling user collateral in the borrow function.
Processing internal event data query for the borrow function.

function _getOracleQuote(uint256 amount) private view returns (uint256)
The Oracle Price value is obtained using the Uniswap Lib.
The getReserves function is called on the uniswapFactory, WETH, DVT contract parameters of the token pair, and returns the sorted results in the order that the parameters are passed.
quote is given some asset amount and reserves, returns an amount of the other asset representing equivalent value.
The quote function returns the amount of another asset representing the same value as a given amount of some asset and reserve. (It is used to calculate the optimal amount of tokens before calling the mint function)
function calculateDepositOfWETHRequired(uint256 tokenAmount) public view returns (uint256)
The optimal amount of tokens is calculated 3 times to determine the Uniswap Oracle Price.
function borrow(uint256 borrowAmount) external
If msg.value is first collateralized three times in the WETH token, the borrowAmount of the token can be borrowed.
The sender must first perform sufficient WETH approval.
It is assumed that the decimal places of WETH and borrowAmount tokens are the same for the calculation.
✅ If the DVT token's balance is greater than or equal to the borrowAmount value received as an argument.
The amount of WETH that the user needs to deposit is calculated by calling the calculateDepositOfWETRequired function with the borrowAmount as an argument.
The user transfers the required WETH to this contract using the WETH Contract's transferFrom function.
Once the collateral is processed, the deposits mapping structure is updated with the user's address internally.
✅ If the DVT token's transfer function is called to transfer borrowAmount to the user.
Event handling is performed.
function calculateDepositOfWETHRequired(uint256 tokenAmount)
public
view
returns (uint256)
{
return (_getOracleQuote(tokenAmount) * 3) / 10**18;
}
// Fetch the price from Uniswap v2 using the official libraries
function _getOracleQuote(uint256 amount) private view returns (uint256) {
(uint256 reservesWETH, uint256 reservesToken) = UniswapV2Library
.getReserves(_uniswapFactory, address(_weth), address(_token));
return
UniswapV2Library.quote(
amount * (10**18),
reservesToken,
reservesWETH
);
}
It is possible to identify vulnerabilities similar to those in the PuppetPool Contract. In the custom PuppetV2Pool.sol code, which depends on Uniswap and WETH9 Contracts, vulnerabilities occur.
The official utility library of Uniswap is being used. Because this function is important for determining the Oracle Price Data feed needed for the actual logic, it can be seen that this logic is in a high-entropy state.
Actually, if you look at the library, it is not much different from the UniswapV1 method.
The amount of loan collateral must also be manipulated in the same way. By selling as many tokens as possible, the collateral for DVT can be increased and the amount of ether can be reduced.
The attacker swaps their DVT tokens for ETH on the liquidity pool in order to manipulate the Oracle Price data feed. Then, the attacker converts the ETH they gained from the swap, along with an additional 30 ETH, into WETH. Finally, the attacker uses the total balance of DVT tokens in the loan protocol pool, calculated by the calculateDepositOfWETHRequired function, to take out a loan and complete the attack.

function swapExactTokensForETH(uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline)
external
returns (uint[] memory amounts);
The function swaps the largest possible amount of ETH according to the path determined by path, ensuring the correct amount of tokens.
The first element of the path must be the input token, the last element must be WETH, and all intermediate elements represent the intermediate pair to be traded. (e.g. if there is no direct pair)
uint amountIn: the base amount of the input token to be sent
uint amountOutMin: the minimum amount of output tokens to be received, and the value must be assigned to prevent the transaction from reverting.
address[] calldata path: an array of token addresses, where path.length must be greater than or equal to 2. There must be a liquidity pool for each consecutive address pair.
address to: the ETH address to receive payment
uint deadline: the time at which the transaction fails, in Unix timestamp
uint[] memory amounts: the amount of input token and all subsequent output amounts
Thank you for the @tinchoabbate that made a good wargame.

The developers of the last lending pool are saying that they've learned the lesson. And just released a new version!
Now they're using a Uniswap v2 exchange as a price oracle, along with the recommended utility libraries. That should be enough.
You start with 20 ETH and 10000 DVT tokens in balance. The new lending pool has a million DVT tokens in balance. You know what to do ;
The CAMM liquidity pool exchange model is based on the Uniswap Protocol v2. The UniswapV2Pair performs the role of an AMM and tracks the balance of the pool tokens (sets up the Oracle Price Data Feed). The UniswapV2Factory holds the general bytecode that supplies power to the factory pair and its main task is to create only one smart contract per unique token pair. The UniswapV2Router, a library used by the router, fully supports all basic requirements of the front-end that provides trading and liquidity management functions, particularly supporting multi-pair trades (e.g. x to y to z) by default and treating ETH as a first-class citizen, and providing meta-trades for liquidity removal.
summary
The attacker is provided with 20 ether.
The DVT and WETH token contracts are deployed.
The UniswapRouter Contract is deployed and creates a DVT-WETH pair with liquidity of 100 DVT and 10 WETH.
The PuppetV2Pool is deployed.
The attacker is provided with 10,000 DVT.
// (uniswap)liquidity : 100DVT, 10ETH
uint256 internal constant UNISWAP_INITIAL_TOKEN_RESERVE = 100e18;
uint256 internal constant UNISWAP_INITIAL_WETH_RESERVE = 10 ether;
// (Pool) : 1,000,000DVT
uint256 internal constant POOL_INITIAL_TOKEN_BALANCE = 1_000_000e18;
uint256 internal constant DEADLINE = 10_000_000;
// (Attacker) : 10,000DVT, 20ETH
uint256 internal constant ATTACKER_INITIAL_TOKEN_BALANCE = 10_000e18;
uint256 internal constant ATTACKER_INITIAL_ETH_BALANCE = 20 ether;
constructor() {
string[] memory labels = new stringUnsupported embed;
labels[0] = "attacker";
preSetup(1, ATTACKER_INITIAL_ETH_BALANCE, labels);
}
function setUp() public override {
super.setUp();
dvt = new DamnValuableToken();
vm.label(address(dvt), "DVT");
weth = new WETH9();
vm.label(address(weth), "WETH");
// (Deploy) Uniswap Factory,Router
uniswapV2Factory = IUniswapV2Factory(
deployCode(
"artifacts/build-uniswap-v2/UniswapV2Factory.json",
abi.encode(address(0))
)
);
uniswapV2Router = IUniswapV2Router02(
deployCode(
"artifacts/build-uniswap-v2/UniswapV2Router02.json",
abi.encode(address(uniswapV2Factory), address(weth))
)
);
The necessary setup process is carried out. The compiled uniswap (factory, Router, pair) and ERC 20 (DVT, WETH) contracts are deployed and the artifacts are integrated.
// (Create) Uniswap pair -- add liquidity
dvt.approve(address(uniswapV2Router), UNISWAP_INITIAL_TOKEN_RESERVE);
uniswapV2Router.addLiquidityETH{value: UNISWAP_INITIAL_WETH_RESERVE}(
address(dvt),
UNISWAP_INITIAL_TOKEN_RESERVE, // amountTokenDesired
0, // amountTokenMin
0, // amountETHMin
address(this), // to
DEADLINE // deadline
);
// getPair ref=> uniswap pair
uniswapV2Pair = IUniswapV2Pair(
uniswapV2Factory.getPair(address(dvt), address(weth))
);
Based on the DVT token contract, the uniswapRouter contract address is set up with initial liquidity by performing an approve operation with a value of UNISWAP_INITIAL_TOKEN_RESERVE: 100 ether.
UniswapV2Router addLiquidityETH
// (Create) Uniswap pair -- add liquidity
dvt.approve(address(uniswapV2Router), UNISWAP_INITIAL_TOKEN_RESERVE);
uniswapV2Router.addLiquidityETH{value: UNISWAP_INITIAL_WETH_RESERVE}(
address(dvt),
UNISWAP_INITIAL_TOKEN_RESERVE, // amountTokenDesired
0, // amountTokenMin
0, // amountETHMin
address(this), // to
DEADLINE // deadline
);
Adds liquidity to an ERC20 ↔ WETH pool with ETH.
To cover all possible scenarios, msg.sender should have already given the router an allowance of at least amountTokenDesired on token.
Always adds assets at the ideal ratio, according to the price when the transaction is executed.
msg.value is treated as a amountETHDesired
Letfover ETH, if any, is returned to msg.sender
if a pool for the passed token and WETH does not exists, one is created automatically, and exactly amountTokenDesired / msg.value tokens are added.
Arguments
address Token
A Pool Token
uint amountTokenDesired
The amount of token to add as liquidity if the WETH/token price is msg.value/amountTokenDesired (token depreciates).
uint amountETHDesired <msg.value>
The amount of ETH to add as liquidity if the token/WETH price is amountTokenDesired/msg.value (WETH depreciates)
uint amountTokenMin
Bounds the extent to which the WETH/token price can go up before the transaction reverts. Must be amountTokenDesired
uint amountETHMin
Bounds the extent to which the token/WETH price can go up before the transaction reverts. Must be msg.value
address to
Recipient of the liquidity tokens
uint deadline
Unix timestamp after which the transaction will revert.
Based on the DVT token contract address, 100 ether is assigned to the amountTokenDesired and Wrapper operation is performed, keeping the current msg.value value at 10 ether under the given conditions.
// getPair ref=> uniswap pair
uniswapV2Pair = IUniswapV2Pair(
uniswapV2Factory.getPair(address(dvt), address(weth))
);
assertGt(uniswapV2Pair.balanceOf(address(this)), 0);
// Deploy the lending pool
puppetV2Pool = new PuppetV2Pool(
address(weth),
address(dvt),
address(uniswapV2Pair),
address(uniswapV2Factory)
);
// Setup initial token balances of pool and attacker account
dvt.transfer(attacker, ATTACKER_INITIAL_TOKEN_BALANCE);
dvt.transfer(address(puppetV2Pool), POOL_INITIAL_TOKEN_BALANCE);
// Ensure correct setup of pool.
assertEq(
puppetV2Pool.calculateDepositOfWETHRequired(1 ether),
0.3 ether
);
assertEq(
puppetV2Pool.calculateDepositOfWETHRequired(
POOL_INITIAL_TOKEN_BALANCE
),
300_000 ether
);
The uniswapV2Exchange contract's getPair function is used to obtain the DVT and WETH token pair contract address, which will be used as an instance.
The uniswapExchange's balance value is checked to see if it is 0.
The attacker account is allocated 10000 ether, and the lending pool contract is allocated 1000000 ether.
PuppetV2Pool.sol

State variables are declared for allocating dependent contract instances.
The deposits mapping data is used to create a mapping structure for handling user collateral in the borrow function.
Processing internal event data query for the borrow function.

function _getOracleQuote(uint256 amount) private view returns (uint256)
The Oracle Price value is obtained using the Uniswap Lib.
The getReserves function is called on the uniswapFactory, WETH, DVT contract parameters of the token pair, and returns the sorted results in the order that the parameters are passed.
quote is given some asset amount and reserves, returns an amount of the other asset representing equivalent value.
The quote function returns the amount of another asset representing the same value as a given amount of some asset and reserve. (It is used to calculate the optimal amount of tokens before calling the mint function)
function calculateDepositOfWETHRequired(uint256 tokenAmount) public view returns (uint256)
The optimal amount of tokens is calculated 3 times to determine the Uniswap Oracle Price.
function borrow(uint256 borrowAmount) external
If msg.value is first collateralized three times in the WETH token, the borrowAmount of the token can be borrowed.
The sender must first perform sufficient WETH approval.
It is assumed that the decimal places of WETH and borrowAmount tokens are the same for the calculation.
✅ If the DVT token's balance is greater than or equal to the borrowAmount value received as an argument.
The amount of WETH that the user needs to deposit is calculated by calling the calculateDepositOfWETRequired function with the borrowAmount as an argument.
The user transfers the required WETH to this contract using the WETH Contract's transferFrom function.
Once the collateral is processed, the deposits mapping structure is updated with the user's address internally.
✅ If the DVT token's transfer function is called to transfer borrowAmount to the user.
Event handling is performed.
function calculateDepositOfWETHRequired(uint256 tokenAmount)
public
view
returns (uint256)
{
return (_getOracleQuote(tokenAmount) * 3) / 10**18;
}
// Fetch the price from Uniswap v2 using the official libraries
function _getOracleQuote(uint256 amount) private view returns (uint256) {
(uint256 reservesWETH, uint256 reservesToken) = UniswapV2Library
.getReserves(_uniswapFactory, address(_weth), address(_token));
return
UniswapV2Library.quote(
amount * (10**18),
reservesToken,
reservesWETH
);
}
It is possible to identify vulnerabilities similar to those in the PuppetPool Contract. In the custom PuppetV2Pool.sol code, which depends on Uniswap and WETH9 Contracts, vulnerabilities occur.
The official utility library of Uniswap is being used. Because this function is important for determining the Oracle Price Data feed needed for the actual logic, it can be seen that this logic is in a high-entropy state.
Actually, if you look at the library, it is not much different from the UniswapV1 method.
The amount of loan collateral must also be manipulated in the same way. By selling as many tokens as possible, the collateral for DVT can be increased and the amount of ether can be reduced.
The attacker swaps their DVT tokens for ETH on the liquidity pool in order to manipulate the Oracle Price data feed. Then, the attacker converts the ETH they gained from the swap, along with an additional 30 ETH, into WETH. Finally, the attacker uses the total balance of DVT tokens in the loan protocol pool, calculated by the calculateDepositOfWETHRequired function, to take out a loan and complete the attack.

function swapExactTokensForETH(uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline)
external
returns (uint[] memory amounts);
The function swaps the largest possible amount of ETH according to the path determined by path, ensuring the correct amount of tokens.
The first element of the path must be the input token, the last element must be WETH, and all intermediate elements represent the intermediate pair to be traded. (e.g. if there is no direct pair)
uint amountIn: the base amount of the input token to be sent
uint amountOutMin: the minimum amount of output tokens to be received, and the value must be assigned to prevent the transaction from reverting.
address[] calldata path: an array of token addresses, where path.length must be greater than or equal to 2. There must be a liquidity pool for each consecutive address pair.
address to: the ETH address to receive payment
uint deadline: the time at which the transaction fails, in Unix timestamp
uint[] memory amounts: the amount of input token and all subsequent output amounts
Thank you for the @tinchoabbate that made a good wargame.
Share Dialog
Share Dialog
No comments yet