ZapToH33

0x9b3e3325d9c9ce09bd9e82cbc0b7ffffc3c47e30

Verification
Verified
v0.8.26+commit.8a97fa7a
Type
Contract
4,249 bytes
ABI entries
20
3 read · 2 write
License
none

Contract information

Address
0x9b3e3325d9c9ce09bd9e82cbc0b7ffffc3c47e30
Chain
Robinhood Chain (4663)
Compiler
v0.8.26+commit.8a97fa7a
Optimization
Enabled
Creation tx
0xdf2d1509fc…3307a6133d

Token

Not a token

This contract does not expose ERC-20 metadata.

Read contract (3)

H33()address
HOOD()address
XHOOD()address

Events (2)

ZappedInZappedOut

ABI

[
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "_hood",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "_xHood",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "_h33",
        "type": "address"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "deadline",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "currentTimestamp",
        "type": "uint256"
      }
    ],
    "name": "DeadlineExpired",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "expectedAsset",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "actualAsset",
        "type": "address"
      }
    ],
    "name": "InvalidH33",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "receiver",
        "type": "address"
      }
    ],
    "name": "InvalidReceiver",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "expectedHood",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "actualHood",
        "type": "address"
      }
    ],
    "name": "InvalidXHood",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "minimum",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "actual",
        "type": "uint256"
      }
    ],
    "name": "MinimumOutputNotMet",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ReentrancyGuardReentrantCall",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "SafeERC20FailedOperation",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "balanceBefore",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "balanceAfter",
        "type": "uint256"
      }
    ],
    "name": "TokenBalanceDecreased",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "expected",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "actual",
        "type": "uint256"
      }
    ],
    "name": "UnexpectedTokenAmount",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ZeroAddress",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ZeroAmount",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ZeroOutput",
    "type": "error"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "caller",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "receiver",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "hoodIn",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "xHoodDeposited",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "h33SharesOut",
        "type": "uint256"
      }
    ],
    "name": "ZappedIn",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "caller",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "receiver",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "h33SharesIn",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "xHoodExited",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "hoodOut",
        "type": "uint256"
      }
    ],
    "name": "ZappedOut",
    "type": "event"
  },
  {
    "inputs": [],
    "name": "H33",
    "outputs": [
      {
        "internalType": "contract IH33Zap",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "HOOD",
    "outputs": [
      {
        "internalType": "contract IERC20",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "XHOOD",
    "outputs": [
      {
        "internalType": "contract IXHoodZap",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "hoodAmount",
        "type": "uint256"
      },
      {
        "internalType": "address",
        "name": "receiver",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "minSharesOut",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "deadline",
        "type": "uint256"
      }
    ],
    "name": "depositHood",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "sharesOut",
        "type": "uint256"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "h33Shares",
        "type": "uint256"
      },
      {
        "internalType": "address",
        "name": "receiver",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "minHoodOut",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "deadline",
        "type": "uint256"
      }
    ],
    "name": "redeemToHood",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "hoodOut",
        "type": "uint256"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  }
]

Source code

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";

interface IXHoodZap is IERC20 {
    function convertEmissionsToken(uint256 amount) external;

    function exit(uint256 amount) external returns (uint256 exitedAmount);

    function hood() external view returns (address);
}

interface IH33Zap is IERC20 {
    function asset() external view returns (address);

    function deposit(
        uint256 assets,
        address receiver
    ) external returns (uint256 shares);

    function redeem(
        uint256 shares,
        address receiver,
        address owner
    ) external returns (uint256 assets);
}

/// @title ZapToH33
/// @notice Permissionless router for entering H33 with HOOD and exiting H33 to HOOD.
/// @dev The contract is intentionally stateless and only grants exact, temporary allowances.
contract ZapToH33 is ReentrancyGuard {
    using SafeERC20 for IERC20;

    IERC20 public immutable HOOD;
    IXHoodZap public immutable XHOOD;
    IH33Zap public immutable H33;

    error ZeroAmount();
    error ZeroOutput();
    error ZeroAddress();
    error InvalidReceiver(address receiver);
    error InvalidXHood(address expectedHood, address actualHood);
    error InvalidH33(address expectedAsset, address actualAsset);
    error DeadlineExpired(uint256 deadline, uint256 currentTimestamp);
    error MinimumOutputNotMet(uint256 minimum, uint256 actual);
    error UnexpectedTokenAmount(
        address token,
        uint256 expected,
        uint256 actual
    );
    error TokenBalanceDecreased(
        address token,
        uint256 balanceBefore,
        uint256 balanceAfter
    );

    event ZappedIn(
        address indexed caller,
        address indexed receiver,
        uint256 hoodIn,
        uint256 xHoodDeposited,
        uint256 h33SharesOut
    );

    event ZappedOut(
        address indexed caller,
        address indexed receiver,
        uint256 h33SharesIn,
        uint256 xHoodExited,
        uint256 hoodOut
    );

    constructor(address _hood, address _xHood, address _h33) {
        if (
            _hood == address(0) ||
            _xHood == address(0) ||
            _h33 == address(0)
        ) revert ZeroAddress();

        HOOD = IERC20(_hood);
        XHOOD = IXHoodZap(_xHood);
        H33 = IH33Zap(_h33);

        address xHoodUnderlying = XHOOD.hood();
        if (xHoodUnderlying != _hood) {
            revert InvalidXHood(_hood, xHoodUnderlying);
        }

        address h33Asset = H33.asset();
        if (h33Asset != _xHood) {
            revert InvalidH33(_xHood, h33Asset);
        }
    }

    modifier beforeDeadline(uint256 deadline) {
        if (block.timestamp > deadline) {
            revert DeadlineExpired(deadline, block.timestamp);
        }
        _;
    }

    /// @notice Converts HOOD into xHOOD and deposits the xHOOD into H33.
    /// @dev The caller must approve this contract to spend at least `hoodAmount` HOOD.
    /// @param hoodAmount Maximum HOOD pulled from the caller.
    /// @param receiver Address receiving the newly minted H33 shares.
    /// @param minSharesOut Minimum acceptable H33 shares minted to `receiver`.
    /// @param deadline Last timestamp at which the transaction may execute.
    function depositHood(
        uint256 hoodAmount,
        address receiver,
        uint256 minSharesOut,
        uint256 deadline
    )
        external
        nonReentrant
        beforeDeadline(deadline)
        returns (uint256 sharesOut)
    {
        if (hoodAmount == 0) revert ZeroAmount();
        _validateReceiver(receiver);

        uint256 hoodBalanceBefore = HOOD.balanceOf(address(this));
        HOOD.safeTransferFrom(msg.sender, address(this), hoodAmount);

        uint256 hoodReceived = _balanceIncrease(
            address(HOOD),
            hoodBalanceBefore,
            HOOD.balanceOf(address(this))
        );
        if (hoodReceived == 0) revert ZeroAmount();

        uint256 xHoodBalanceBefore = XHOOD.balanceOf(address(this));
        uint256 xHoodBackingBefore = HOOD.balanceOf(address(XHOOD));

        HOOD.forceApprove(address(XHOOD), hoodReceived);
        XHOOD.convertEmissionsToken(hoodReceived);
        HOOD.forceApprove(address(XHOOD), 0);

        uint256 xHoodMinted = _balanceIncrease(
            address(XHOOD),
            xHoodBalanceBefore,
            XHOOD.balanceOf(address(this))
        );
        if (xHoodMinted != hoodReceived) {
            revert UnexpectedTokenAmount(
                address(XHOOD),
                hoodReceived,
                xHoodMinted
            );
        }

        uint256 backingIncrease = _balanceIncrease(
            address(HOOD),
            xHoodBackingBefore,
            HOOD.balanceOf(address(XHOOD))
        );
        if (backingIncrease != hoodReceived) {
            revert UnexpectedTokenAmount(
                address(HOOD),
                hoodReceived,
                backingIncrease
            );
        }

        _requireBalance(address(HOOD), hoodBalanceBefore);

        uint256 receiverSharesBefore = H33.balanceOf(receiver);
        IERC20(address(XHOOD)).forceApprove(address(H33), xHoodMinted);
        sharesOut = H33.deposit(xHoodMinted, receiver);
        IERC20(address(XHOOD)).forceApprove(address(H33), 0);

        uint256 receiverSharesMinted = _balanceIncrease(
            address(H33),
            receiverSharesBefore,
            H33.balanceOf(receiver)
        );
        if (receiverSharesMinted != sharesOut) {
            revert UnexpectedTokenAmount(
                address(H33),
                sharesOut,
                receiverSharesMinted
            );
        }
        if (sharesOut == 0) revert ZeroOutput();
        if (sharesOut < minSharesOut) {
            revert MinimumOutputNotMet(minSharesOut, sharesOut);
        }

        _requireBalance(address(XHOOD), xHoodBalanceBefore);
        _requireBalance(address(HOOD), hoodBalanceBefore);

        emit ZappedIn(
            msg.sender,
            receiver,
            hoodReceived,
            xHoodMinted,
            sharesOut
        );
    }

    /// @notice Redeems H33 shares, exits the received xHOOD, and sends HOOD to `receiver`.
    /// @dev The caller must approve this contract to spend at least `h33Shares` H33.
    ///      The xHOOD exit penalty configured in the xHOOD contract applies.
    /// @param h33Shares H33 shares burned from the caller.
    /// @param receiver Address receiving the resulting HOOD.
    /// @param minHoodOut Minimum acceptable HOOD received by `receiver`.
    /// @param deadline Last timestamp at which the transaction may execute.
    function redeemToHood(
        uint256 h33Shares,
        address receiver,
        uint256 minHoodOut,
        uint256 deadline
    )
        external
        nonReentrant
        beforeDeadline(deadline)
        returns (uint256 hoodOut)
    {
        if (h33Shares == 0) revert ZeroAmount();
        _validateReceiver(receiver);

        uint256 xHoodBalanceBefore = XHOOD.balanceOf(address(this));
        uint256 hoodBalanceBefore = HOOD.balanceOf(address(this));

        uint256 xHoodReported = H33.redeem(
            h33Shares,
            address(this),
            msg.sender
        );
        uint256 xHoodReceived = _balanceIncrease(
            address(XHOOD),
            xHoodBalanceBefore,
            XHOOD.balanceOf(address(this))
        );
        if (xHoodReceived != xHoodReported) {
            revert UnexpectedTokenAmount(
                address(XHOOD),
                xHoodReported,
                xHoodReceived
            );
        }
        if (xHoodReceived == 0) revert ZeroOutput();

        uint256 hoodReported = XHOOD.exit(xHoodReceived);
        uint256 hoodReceived = _balanceIncrease(
            address(HOOD),
            hoodBalanceBefore,
            HOOD.balanceOf(address(this))
        );
        if (hoodReceived != hoodReported) {
            revert UnexpectedTokenAmount(
                address(HOOD),
                hoodReported,
                hoodReceived
            );
        }

        _requireBalance(address(XHOOD), xHoodBalanceBefore);

        uint256 receiverHoodBefore = HOOD.balanceOf(receiver);
        HOOD.safeTransfer(receiver, hoodReceived);
        hoodOut = _balanceIncrease(
            address(HOOD),
            receiverHoodBefore,
            HOOD.balanceOf(receiver)
        );

        if (hoodOut == 0) revert ZeroOutput();
        if (hoodOut < minHoodOut) {
            revert MinimumOutputNotMet(minHoodOut, hoodOut);
        }

        _requireBalance(address(HOOD), hoodBalanceBefore);

        emit ZappedOut(
            msg.sender,
            receiver,
            h33Shares,
            xHoodReceived,
            hoodOut
        );
    }

    function _validateReceiver(address receiver) internal view {
        if (receiver == address(0) || receiver == address(this)) {
            revert InvalidReceiver(receiver);
        }
    }

    function _requireBalance(address token, uint256 expected) internal view {
        uint256 actual = IERC20(token).balanceOf(address(this));
        if (actual != expected) {
            revert UnexpectedTokenAmount(token, expected, actual);
        }
    }

    function _balanceIncrease(
        address token,
        uint256 balanceBefore,
        uint256 balanceAfter
    ) internal pure returns (uint256 increase) {
        if (balanceAfter < balanceBefore) {
            revert TokenBalanceDecreased(token, balanceBefore, balanceAfter);
        }
        return balanceAfter - balanceBefore;
    }
}
Chain explorer5320msChain node97ms