Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 12 additions & 5 deletions script/DeployMaxBTCERC20.script.sol
Original file line number Diff line number Diff line change
@@ -1,20 +1,26 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

import { Script } from "forge-std/Script.sol";
import { console } from "forge-std/console.sol";
import { MaxBTCERC20 } from "../src/MaxBTCERC20.sol";
import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
import {Script} from "forge-std/Script.sol";
import {console} from "forge-std/console.sol";
import {MaxBTCERC20} from "../src/MaxBTCERC20.sol";
import {
ERC1967Proxy
} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";

contract DeployMaxBTCERC20 is Script {
function run() external {
address implementation = vm.envAddress("IMPLEMENTATION");
address owner = vm.envAddress("OWNER");
address ics20 = vm.envAddress("ICS20");
address core = vm.envAddress("CORE");
string memory name = vm.envString("TOKEN_NAME");
string memory symbol = vm.envString("TOKEN_SYMBOL");

bytes memory initializeCall = abi.encodeCall(MaxBTCERC20.initialize, (owner, ics20, name, symbol));
bytes memory initializeCall = abi.encodeCall(
MaxBTCERC20.initialize,
(owner, ics20, core, name, symbol)
);

vm.startBroadcast();
ERC1967Proxy proxy = new ERC1967Proxy(implementation, initializeCall);
Expand All @@ -25,6 +31,7 @@ contract DeployMaxBTCERC20 is Script {
console.log(" Implementation: ", implementation);
console.log(" Owner: ", owner);
console.log(" ICS20: ", ics20);
console.log(" CORE: ", core);
console.log(" Name: ", name);
console.log(" Symbol: ", symbol);
}
Expand Down
252 changes: 252 additions & 0 deletions src/MaxBTCCore.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.28;

import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
import {Receiver} from "./Receiver.sol";
import {MaxBTCERC20} from "./MaxBTCERC20.sol";
import {WithdrawalToken} from "./WithdrawalToken.sol";
import {Batch} from "./types/CoreTypes.sol";

contract MaxBTCCore is Initializable, UUPSUpgradeable, OwnableUpgradeable {
struct CoreConfig {
address depositToken;
address maxBtcToken;
address withdrawalToken;
address exchangeRateReceiver;
uint256 exchangeRateStalePeriod;
}

/// @notice Events

event Deposit(
address indexed depositor,
uint256 depositAmount,
uint256 maxBtcMinted
);

event Withdrawal(
address indexed withdrawer,
uint256 maxBtcBurned,
uint256 batchId
);

/// @notice Errors

error InvalidDepositTokenAddress();
error InvalidMaxBTCTokenAddress();
error InvalidWithdrawalTokenAddress();
error InvalidExchangeRateReceiverAddress();
error ExchangeRateStale();
error WithdrawingBatchAlreadyExists();
error WithdrawingBatchMissing();
error FinalizedBatchMissing(uint256 batchId);

/// @dev keccak256(abi.encode(uint256(keccak256("maxbtc.core.config")) - 1)) & ~bytes32(uint256(0xff))
bytes32 private constant CONFIG_STORAGE_SLOT =
0xe8041c5a119ce847809f9491390b5e4b81852379983e998195264ecb0ca5b100;
/// @dev keccak256(abi.encode(uint256(keccak256("maxbtc.core.batch_state")) - 1)) & ~bytes32(uint256(0xff))
bytes32 private constant BATCH_STATE_STORAGE_SLOT =
0xcd680cc7c8e435be1f7479ad5e3bda309608af714cd2b7b35d4d58c3c8569700;
/// @dev keccak256(abi.encode(uint256(keccak256("maxbtc.core.finalized_batches")) - 1)) & ~bytes32(uint256(0xff))
bytes32 private constant FINALIZED_BATCHES_STORAGE_SLOT =
0x6ba6b86991a1f4fd0c4351857af540e99efdf5c523d2e0e4d1a5236d81710f00;

struct BatchState {
Batch activeBatch;
Batch withdrawingBatch;
bool hasWithdrawingBatch;
}

struct FinalizedBatchesStorage {
mapping(uint256 => Batch) batches;
uint256[] finalizedBatchIds;
}

function _getCoreConfig() private pure returns (CoreConfig storage $) {
assembly {
$.slot := CONFIG_STORAGE_SLOT
}
}

function initialize(
address owner,
address _depositToken,
address _maxBtcToken,
address _withdrawalToken,
address _exchangeRateReceiver,
uint256 _exchangeRateStalePeriod
) public initializer {
__Ownable_init(owner);
if (_depositToken == address(0)) {
revert InvalidDepositTokenAddress();
}
if (_maxBtcToken == address(0)) {
revert InvalidMaxBTCTokenAddress();
}
if (_withdrawalToken == address(0)) {
revert InvalidWithdrawalTokenAddress();
}
if (_exchangeRateReceiver == address(0)) {
revert InvalidExchangeRateReceiverAddress();
}
CoreConfig storage config = _getCoreConfig();
config.depositToken = _depositToken;
config.maxBtcToken = _maxBtcToken;
config.withdrawalToken = _withdrawalToken;
config.exchangeRateReceiver = _exchangeRateReceiver;
config.exchangeRateStalePeriod = _exchangeRateStalePeriod;

BatchState storage batchState = _getBatchState();
batchState.activeBatch = _createNewBatch(0);
}

function _getBatchState() private pure returns (BatchState storage $) {
assembly {
$.slot := BATCH_STATE_STORAGE_SLOT
}
}

function _getFinalizedBatchesStorage()
private
pure
returns (FinalizedBatchesStorage storage $)
{
assembly {
$.slot := FINALIZED_BATCHES_STORAGE_SLOT
}
}

function _depositDecimals() private view returns (uint256) {
CoreConfig storage config = _getCoreConfig();
return uint256(IERC20Metadata(config.depositToken).decimals());
}

function _createNewBatch(
uint256 batchId
) private view returns (Batch memory) {
return
Batch({
batchId: batchId,
btcRequested: 0,
maxBtcBurned: 0,
collectedAmount: 0,
depositDecimals: _depositDecimals()
});
}

function _activeBatch() private view returns (Batch storage) {
BatchState storage batchState = _getBatchState();
return batchState.activeBatch;
}

function activeBatch() external view returns (Batch memory) {
Batch storage currentBatch = _activeBatch();
return currentBatch;
}

function withdrawingBatch() external view returns (Batch memory, bool) {
BatchState storage batchState = _getBatchState();
return (batchState.withdrawingBatch, batchState.hasWithdrawingBatch);
}

function finalizedBatch(
uint256 batchId
) public view returns (Batch memory) {
FinalizedBatchesStorage
storage finalized = _getFinalizedBatchesStorage();
Batch memory batch = finalized.batches[batchId];
if (batch.depositDecimals == 0) {
revert FinalizedBatchMissing(batchId);
}
return batch;
}

function finalizedBatches() external view returns (Batch[] memory) {
FinalizedBatchesStorage
storage finalized = _getFinalizedBatchesStorage();
uint256 length = finalized.finalizedBatchIds.length;
Batch[] memory batches = new Batch[](length);
for (uint256 i = 0; i < length; i++) {
uint256 batchId = finalized.finalizedBatchIds[i];
batches[i] = finalized.batches[batchId];
}
return batches;
}

function _addFinalizedBatch(Batch memory batch) internal {
FinalizedBatchesStorage
storage finalized = _getFinalizedBatchesStorage();
finalized.batches[batch.batchId] = batch;
finalized.finalizedBatchIds.push(batch.batchId);
}

function moveActiveBatchToWithdrawing() external onlyOwner {
BatchState storage batchState = _getBatchState();
if (batchState.hasWithdrawingBatch) {
revert WithdrawingBatchAlreadyExists();
}

batchState.withdrawingBatch = batchState.activeBatch;
batchState.hasWithdrawingBatch = true;
batchState.activeBatch = _createNewBatch(
batchState.activeBatch.batchId + 1
);
}

function finalizeWithdrawingBatch(
uint256 collectedAmount
) external onlyOwner {
BatchState storage batchState = _getBatchState();
if (!batchState.hasWithdrawingBatch) {
revert WithdrawingBatchMissing();
}
batchState.withdrawingBatch.collectedAmount = collectedAmount;
_addFinalizedBatch(batchState.withdrawingBatch);
delete batchState.withdrawingBatch;
batchState.hasWithdrawingBatch = false;
}

function deposit(uint256 amount) external {
CoreConfig storage config = _getCoreConfig();
SafeERC20.safeTransferFrom(
IERC20(config.depositToken),
msg.sender,
address(this),
amount
);
(uint256 exchangeRate, uint256 lastUpdated) = Receiver(
config.exchangeRateReceiver
).getLatest();
require(
block.timestamp - lastUpdated < config.exchangeRateStalePeriod,
ExchangeRateStale()
);
uint256 maxBtcToMint = (amount * 1e18) / exchangeRate;
MaxBTCERC20(config.maxBtcToken).mint(msg.sender, maxBtcToMint);
emit Deposit(msg.sender, amount, maxBtcToMint);
}

function withdraw(uint256 maxBtcAmount) external {
CoreConfig storage config = _getCoreConfig();
Batch storage batch = _activeBatch();
batch.maxBtcBurned += maxBtcAmount;
MaxBTCERC20(config.maxBtcToken).burn(_msgSender(), maxBtcAmount);
uint256 batchId = batch.batchId;
WithdrawalToken(config.withdrawalToken).mint(
_msgSender(),
batchId,
maxBtcAmount,
""
);
emit Withdrawal(_msgSender(), maxBtcAmount, batchId);
}

function _authorizeUpgrade(
address newImplementation
) internal override onlyOwner {}
}
Loading