Last active
July 22, 2025 21:25
-
-
Save rfikki/40b99bba43dc9d9822fda8149c91abae 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.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