Skip to content

Instantly share code, notes, and snippets.

@rfikki
Last active May 15, 2026 22:49
Show Gist options
  • Select an option

  • Save rfikki/b38c6800c740d57aee332fce120fed71 to your computer and use it in GitHub Desktop.

Select an option

Save rfikki/b38c6800c740d57aee332fce120fed71 to your computer and use it in GitHub Desktop.
// 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