Last active
May 15, 2026 22:49
-
-
Save rfikki/b38c6800c740d57aee332fce120fed71 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // SPDX-License-Identifier: MIT | |
| pragma solidity ^0.8.24; | |
| import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; | |
| import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; | |
| import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; | |
| import "@openzeppelin/contracts/utils/Pausable.sol"; | |
| import "@openzeppelin/contracts/access/Ownable.sol"; | |
| /** | |
| * @title DropBox Vault for Legacy DINO | |
| * @author rfikki | |
| * @notice A minimal personal vault used to collect 2015 Legacy Dinero tokens. | |
| * @dev Required because the 2015 DINO contract (0x3746...) lacks standard | |
| * ERC20 functions like approve() and transferFrom(). This contract acts as | |
| * a deterministic intermediary that the Wrapper can sweep. | |
| */ | |
| contract DropBox { | |
| /// @notice The address of the main WrappedDinero contract authorized to sweep funds. | |
| address public immutable wrapper; | |
| /** | |
| * @param _wrapper The address of the WrappedDinero contract. | |
| */ | |
| constructor(address _wrapper) { | |
| wrapper = _wrapper; | |
| } | |
| /** | |
| * @notice Transfers held legacy tokens to the main wrapper. | |
| * @dev Only callable by the wrapper address defined at deployment. | |
| * @param token The IERC20 interface of the legacy DINO token. | |
| * @param amount The number of units (2 decimals) to transfer. | |
| */ | |
| function collect(IERC20 token, uint256 amount) external { | |
| require(msg.sender == wrapper, "Only wrapper can collect"); | |
| require(token.transfer(wrapper, amount), "Legacy transfer failed"); | |
| } | |
| } | |
| /** | |
| * @title Wrapped Dinero (DINO) | |
| * @author rfikki | |
| * @notice Bridges 2015 Legacy Dinero to modern ERC20 standards with 18 decimals. | |
| * @dev This contract uses CREATE2 to deploy personal vaults (DropBox) for users, | |
| * allowing them to "wrap" their tokens without an approve() call. It includes | |
| * a gamification hook for Upeg-style NFT systems based on a 100:1 ratio. | |
| */ | |
| contract WrappedDinero is ERC20, ReentrancyGuard, Pausable, Ownable { | |
| /// @notice Address of the original 2015 Dinero token contract. | |
| IERC20 public constant LEGACY_DINO = IERC20(0x374642afe485d1a181B01F5b028d169BE58f3106); | |
| /** | |
| * @dev Multiplier to convert 2-decimal units to 18-decimal units. | |
| * Calculation: 10^18 (Modern) / 10^2 (Legacy) = 10^16. | |
| */ | |
| uint256 private constant DECIMAL_MULTIPLIER = 10**16; | |
| /** | |
| * @notice The threshold of wDINO required to "sequence" one Dinosaur NFT. | |
| * @dev Based on 1,000,000 total supply and 10,000 NFT capacity (1,000,000 / 10,000 = 100). | |
| */ | |
| uint256 public constant TOKENS_PER_DINOSAUR = 100 * 10**18; | |
| /** | |
| * @notice Emitted when legacy tokens are successfully wrapped. | |
| * @param user The address of the user performing the wrap. | |
| * @param legacyAmount The amount of legacy tokens (2 decimals) deposited. | |
| * @param wrappedAmount The amount of wDINO tokens (18 decimals) minted. | |
| */ | |
| event Wrapped(address indexed user, uint256 legacyAmount, uint256 wrappedAmount); | |
| /** | |
| * @notice Emitted when wDINO is burned to reclaim legacy tokens. | |
| * @param user The address of the user performing the unwrap. | |
| * @param wrappedAmount The amount of wDINO (18 decimals) burned. | |
| * @param legacyAmount The amount of legacy tokens (2 decimals) returned. | |
| */ | |
| event Unwrapped(address indexed user, uint256 wrappedAmount, uint256 legacyAmount); | |
| /** | |
| * @notice Signals a change in the user's "Whole Dinosaur" count. | |
| * @dev This is the primary event used by Uniswap V4 Upeg hooks to update dynamic NFT state. | |
| * @param user The address of the holder. | |
| * @param dinosaurCount The number of full 100-token units held by the user. | |
| */ | |
| event DinosaurSync(address indexed user, uint256 dinosaurCount); | |
| /** | |
| * @notice Initializes the contract and sets the name/symbol to "Wrapped Dinero" / "DINO". | |
| */ | |
| constructor() ERC20("Wrapped Dinero", "DINO") Ownable(msg.sender) {} | |
| /** | |
| * @notice Predicts the deterministic vault address for any user. | |
| * @dev Users must transfer their legacy DINO to this address before calling wrap(). | |
| * @param user The wallet address of the user. | |
| * @return The calculated address of the user's DropBox vault. | |
| */ | |
| function getDropBoxAddress(address user) public view returns (address) { | |
| bytes32 salt = keccak256(abi.encodePacked(user)); | |
| bytes32 hash = keccak256( | |
| abi.encodePacked( | |
| bytes1(0xff), | |
| address(this), | |
| salt, | |
| keccak256(abi.encodePacked(type(DropBox).creationCode, abi.encode(address(this)))) | |
| ) | |
| ); | |
| return address(uint160(uint256(hash))); | |
| } | |
| /** | |
| * @notice Deploys a user's vault (if needed), sweeps legacy DINO, and mints 18-decimal wDINO. | |
| * @dev Uses CREATE2 for deterministic deployment. Multiplies input by 10^16. | |
| */ | |
| function wrap() external nonReentrant whenNotPaused { | |
| address dropBoxAddr = getDropBoxAddress(msg.sender); | |
| uint256 legacyBalance = LEGACY_DINO.balanceOf(dropBoxAddr); | |
| require(legacyBalance > 0, "No legacy DINO found in vault"); | |
| _deployDropBoxIfNeeded(msg.sender, dropBoxAddr); | |
| // Collect 2-decimal units from the vault | |
| DropBox(dropBoxAddr).collect(LEGACY_DINO, legacyBalance); | |
| // Mint 18-decimal units to the user | |
| uint256 wrappedAmount = legacyBalance * DECIMAL_MULTIPLIER; | |
| _mint(msg.sender, wrappedAmount); | |
| emit Wrapped(msg.sender, legacyBalance, wrappedAmount); | |
| _syncDinosaurCount(msg.sender); | |
| } | |
| /** | |
| * @notice Burns 18-decimal wDINO to release 2-decimal legacy DINO. | |
| * @dev Only allows unwrapping amounts that align with whole legacy units to prevent dust loss. | |
| * @param wrappedAmount The amount of wDINO to burn (must be a multiple of 10^16). | |
| */ | |
| function unwrap(uint256 wrappedAmount) external nonReentrant whenNotPaused { | |
| require(balanceOf(msg.sender) >= wrappedAmount, "Insufficient DINO balance"); | |
| require(wrappedAmount % DECIMAL_MULTIPLIER == 0, "Amount must align with legacy units"); | |
| uint256 legacyAmount = wrappedAmount / DECIMAL_MULTIPLIER; | |
| _burn(msg.sender, wrappedAmount); | |
| require(LEGACY_DINO.transfer(msg.sender, legacyAmount), "Legacy transfer failed"); | |
| emit Unwrapped(msg.sender, wrappedAmount, legacyAmount); | |
| _syncDinosaurCount(msg.sender); | |
| } | |
| /** | |
| * @dev Internal helper to deploy the DropBox via CREATE2. | |
| * @param user The user address used to generate the salt. | |
| * @param expected The expected address to check for existing code. | |
| */ | |
| function _deployDropBoxIfNeeded(address user, address expected) internal { | |
| uint256 size; | |
| assembly { size := extcodesize(expected) } | |
| if (size == 0) { | |
| bytes32 salt = keccak256(abi.encodePacked(user)); | |
| new DropBox{salt: salt}(address(this)); | |
| } | |
| } | |
| /** | |
| * @dev Emits the DinosaurSync event based on the user's current 18-decimal balance. | |
| * @param user The address to calculate the dinosaur count for. | |
| */ | |
| function _syncDinosaurCount(address user) internal { | |
| uint256 count = balanceOf(user) / TOKENS_PER_DINOSAUR; | |
| emit DinosaurSync(user, count); | |
| } | |
| /** | |
| * @dev Overrides the standard ERC20 _update function to trigger gamification syncs on every transfer. | |
| * @param from Address sending the tokens. | |
| * @param to Address receiving the tokens. | |
| * @param value Amount of tokens transferred. | |
| */ | |
| function _update(address from, address to, uint256 value) internal virtual override { | |
| super._update(from, to, value); | |
| if (from != address(0)) _syncDinosaurCount(from); | |
| if (to != address(0)) _syncDinosaurCount(to); | |
| } | |
| /** | |
| * @notice Pauses all wrapping and unwrapping actions. | |
| * @dev Only callable by the contract owner. | |
| */ | |
| function pause() external onlyOwner { _pause(); } | |
| /** | |
| * @notice Unpauses wrapping and unwrapping actions. | |
| * @dev Only callable by the contract owner. | |
| */ | |
| function unpause() external onlyOwner { _unpause(); } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment