Skip to content

Instantly share code, notes, and snippets.

@rfikki
Last active July 22, 2025 21:25
Show Gist options
  • Save rfikki/40b99bba43dc9d9822fda8149c91abae to your computer and use it in GitHub Desktop.
Save rfikki/40b99bba43dc9d9822fda8149c91abae to your computer and use it in GitHub Desktop.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
/**
* @title Interface for the external CurrencyCoin contract.
* @dev This interface defines the functions that the WrappedCurrencyCoin contract
* will call on the original 2015 Currency.sol contract.
*/
interface ICurrencyCoin {
/**
* @notice Returns the balance of CurrencyCoin for a given address.
* @param _owner The address to query the balance of.
* @return The balance of the address.
*/
function coinBalanceOf(address _owner) external view returns (uint256);
/**
* @notice Sends a specified amount of CurrencyCoin to a receiver.
* @param _amount The amount of coin to send.
* @param _receiver The address of the recipient.
*/
function sendCoin(uint256 _amount, address _receiver) external;
}
/**
* @title DropBox Contract
* @dev A temporary holding contract for a user's CurrencyCoins before wrapping.
* Each user gets a unique DropBox, owned by the main WrappedCurrencyCoin contract.
* This pattern ensures that users' funds are isolated and the main contract has
* the necessary permissions to pull the funds during the wrapping process.
*/
contract DropBox is Ownable(msg.sender) {
/**
* @dev Sets the initial owner of the DropBox contract.
* @param initialOwner The address of the owner, which should be the WrappedCurrencyCoin contract.
*/
constructor(address initialOwner) Ownable(initialOwner) {}
/**
* @notice Allows the owner (WrappedCurrencyCoin contract) to collect CurrencyCoins.
* @dev This function is called during the `wrap` process.
* @param value The amount of CurrencyCoin to collect.
* @param ccInt The interface to the CurrencyCoin contract.
*/
function collect(uint256 value, ICurrencyCoin ccInt) public onlyOwner {
// Sends the coins from this DropBox to its owner (the WrappedCurrencyCoin contract).
ccInt.sendCoin(value, owner());
}
}
/**
* @title WrappedCurrencyCoin (CC)
* @dev An ERC20 token that represents a wrapped version of CurrencyCoin.
* This contract allows users to wrap their 2015 Currency.sol tokens into a standard ERC20 token
* and unwrap it back to the original coin. It uses a DropBox system for security.
*/
contract WrappedCurrencyCoin is ERC20, Ownable {
// --- Events ---
event DropBoxCreated(address indexed user, address indexed dropBoxAddress);
event Wrapped(address indexed user, uint256 value);
event Unwrapped(address indexed user, uint256 value);
// --- Constants ---
/**
* @dev The hardcoded address of the 2015 CurrencyCoin contract.
*/
address private constant CURRENCY_COIN_2015_ADDRESS = 0x8494F777d13503BE928BB22b1F4ae3289E634FD3;
// --- State Variables ---
ICurrencyCoin public constant currencyCoin = ICurrencyCoin(CURRENCY_COIN_2015_ADDRESS);
/**
* @dev Mapping from a user's address to their unique DropBox contract address.
* This is private to prevent one user from seeing another's DropBox address.
*/
mapping(address => address) private dropBoxes;
// --- Constructor ---
/**
* @dev Initializes the contract and sets the owner.
*/
constructor() ERC20("Wrapped CurrencyCoin", "CC") Ownable(msg.sender) {}
// --- Public Functions ---
/**
* @notice Returns the DropBox address created by the caller.
* @dev Returns address(0) if the caller has not created a DropBox yet.
* This function ensures that users can only view their own DropBox address.
* @return The address of the caller's DropBox contract.
*/
function getMyDropBoxAddress() public view returns (address) {
return dropBoxes[msg.sender];
}
/**
* @notice Creates a unique DropBox contract for the caller.
* @dev The user must send CurrencyCoins to this new DropBox address
* before they can be wrapped into CC. The DropBox is owned by this contract.
*/
function createDropBox() public {
require(dropBoxes[msg.sender] == address(0), "CC: Drop box already exists");
// Create a new DropBox and set this contract as its owner.
// This is necessary so this contract can call the `onlyOwner` `collect` function.
address newDropBoxAddress = address(new DropBox(address(this)));
dropBoxes[msg.sender] = newDropBoxAddress;
emit DropBoxCreated(msg.sender, newDropBoxAddress);
}
/**
* @notice Wraps CurrencyCoin into CC tokens.
* @dev The user (msg.sender) must have first sent CurrencyCoin to their personal DropBox address.
* This function will pull the specified amount from the caller's DropBox and mint CC tokens to them.
* @param value The amount of CurrencyCoin to wrap (in its smallest unit).
*/
function wrap(uint256 value) public {
address dropBoxAddress = dropBoxes[msg.sender];
require(dropBoxAddress != address(0), "CC: You must create a drop box first");
uint256 dropBoxBalance = currencyCoin.coinBalanceOf(dropBoxAddress);
require(dropBoxBalance >= value, "CC: Not enough coins in drop box to wrap");
// The DropBox contract is owned by this contract, allowing this call.
DropBox(dropBoxAddress).collect(value, currencyCoin);
_mint(msg.sender, value);
emit Wrapped(msg.sender, value);
}
/**
* @notice Unwraps CC back into the original 2015 CurrencyCoin.
* @dev This function burns the caller's (msg.sender's) CC and sends them the equivalent
* amount of CurrencyCoin from this contract's holdings.
* @param value The amount of CC to unwrap.
*/
function unwrap(uint256 value) public {
require(balanceOf(msg.sender) >= value, "CC: Not enough CC to unwrap");
_burn(msg.sender, value);
currencyCoin.sendCoin(value, msg.sender);
emit Unwrapped(msg.sender, value);
}
// --- Overrides ---
/**
* @dev CC has no decimals, matching the likely nature of the original coin.
*/
function decimals() public pure override returns (uint8) {
return 0;
}
// --- Owner-only Functions ---
/**
* @notice Allows the contract owner to withdraw any CurrencyCoins that were
* sent to this contract directly, bypassing the wrapping mechanism.
* @dev This is a safety measure to prevent funds from being locked. The owner can only
* rescue tokens that are not backing the total supply of CC.
* @param amount The amount of CurrencyCoin to rescue.
*/
function rescueTokens(uint256 amount) external onlyOwner {
// Calculate the amount of CurrencyCoin held by this contract that is NOT backing wrapped tokens.
uint256 totalCCInContract = currencyCoin.coinBalanceOf(address(this));
uint256 wrappedSupply = totalSupply();
// This check ensures we don't withdraw tokens that are backing existing CC.
require(totalCCInContract >= wrappedSupply, "CC: Inconsistent contract state");
uint256 availableToRescue = totalCCInContract - wrappedSupply;
require(amount <= availableToRescue, "CC: Amount exceeds rescuable tokens");
currencyCoin.sendCoin(amount, owner());
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment