Skip to content

Commit e946697

Browse files
committed
(feat) (openai/gpt-5.5, reviewed T, tested T) add fcf b20 wrapper
1 parent e436eb9 commit e946697

4 files changed

Lines changed: 1159 additions & 0 deletions

File tree

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
BASE_SEPOLIA_RPC_URL="${BASE_SEPOLIA_RPC_URL:-https://sepolia.base.org}"
5+
VERIFY="${VERIFY:-true}"
6+
CONTRACTS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
7+
8+
if [[ -z "${PRIVATE_KEY:-}" ]]; then
9+
printf 'Missing PRIVATE_KEY.\n' >&2
10+
exit 1
11+
fi
12+
13+
if [[ -z "${TREASURY_ADDRESS:-}" ]]; then
14+
printf 'Missing TREASURY_ADDRESS.\n' >&2
15+
exit 1
16+
fi
17+
18+
ARGS=(
19+
"$CONTRACTS_DIR/script/DeployFCFWrapper.s.sol:DeployFCFWrapper"
20+
--root "$CONTRACTS_DIR"
21+
--rpc-url "$BASE_SEPOLIA_RPC_URL"
22+
--broadcast
23+
)
24+
25+
if [[ "$VERIFY" == "true" ]]; then
26+
ARGS+=(--verify)
27+
fi
28+
29+
forge script "${ARGS[@]}"
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// script/DeployFCFWrapper.s.sol
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
pragma solidity ^0.8.24;
5+
6+
import {Script} from "forge-std/Script.sol";
7+
import {FCFWrapper} from "../src/b20/FCFWrapper.sol";
8+
9+
contract DeployFCFWrapper is Script {
10+
function run() external returns (FCFWrapper wrapper) {
11+
uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
12+
address treasury = vm.envAddress("TREASURY_ADDRESS");
13+
bytes32 salt = vm.envOr("FCF_WRAPPER_SALT", bytes32(0));
14+
15+
vm.startBroadcast(deployerPrivateKey);
16+
wrapper = new FCFWrapper(treasury, salt);
17+
vm.stopBroadcast();
18+
}
19+
}

contracts/src/b20/FCFWrapper.sol

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
3+
pragma solidity ^0.8.24;
4+
5+
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
6+
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
7+
import {Context} from "@openzeppelin/contracts/utils/Context.sol";
8+
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
9+
import {IB20} from "base-std/interfaces/IB20.sol";
10+
import {IB20Factory} from "base-std/interfaces/IB20Factory.sol";
11+
import {B20Constants} from "base-std/lib/B20Constants.sol";
12+
import {B20FactoryLib} from "base-std/lib/B20FactoryLib.sol";
13+
import {StdPrecompiles} from "base-std/StdPrecompiles.sol";
14+
15+
/**
16+
* @title FCFWrapper
17+
* @notice Immutable 1:1 wrapper between the Base fcf ERC-20 and a freshly deployed B20 Asset token.
18+
*/
19+
contract FCFWrapper is Context, ReentrancyGuard {
20+
using SafeERC20 for IERC20;
21+
22+
address private constant _FCF_TOKEN = 0x67A7CA081Dc79B45fD1FA059Cd3b8dCcA779Aba3;
23+
string private constant _WRAPPED_NAME = "Wrapped fcf";
24+
string private constant _WRAPPED_SYMBOL = "wFCF";
25+
uint8 private constant _WRAPPED_DECIMALS = 18;
26+
27+
IERC20 private immutable _underlying;
28+
IB20 private immutable _wrappedB20;
29+
address private immutable _treasury;
30+
31+
error FCFWrapperInvalidReceiver(address receiver);
32+
error FCFWrapperInvalidTreasury(address treasury);
33+
error FCFWrapperInvalidRecoverRecipient(address recipient, address treasury);
34+
error FCFWrapperNoUnderlyingReceived(uint256 balanceBefore, uint256 balanceAfter);
35+
error FCFWrapperUnauthorizedRecover(address caller, address treasury);
36+
error FCFWrapperZeroAmount();
37+
38+
event FCFDeposited(address indexed caller, address indexed receiver, uint256 requested, uint256 minted);
39+
event FCFWithdrawn(address indexed caller, address indexed receiver, uint256 amount);
40+
event FCFRecovered(address indexed caller, address indexed treasury, uint256 amount);
41+
42+
/**
43+
* @dev Deploys the paired B20 Asset token and permanently removes its default admin.
44+
*
45+
* Requirements:
46+
*
47+
* - `treasury_` must not be the zero address.
48+
* - `salt` must not collide with an existing B20 Asset for this wrapper address.
49+
*
50+
* Emits the B20 factory and B20 role events during construction.
51+
*/
52+
constructor(address treasury_, bytes32 salt) {
53+
if (treasury_ == address(0)) revert FCFWrapperInvalidTreasury(treasury_);
54+
55+
_underlying = IERC20(_FCF_TOKEN);
56+
_treasury = treasury_;
57+
58+
bytes[] memory initCalls = new bytes[](3);
59+
initCalls[0] = B20FactoryLib.encodeGrantRole(B20Constants.MINT_ROLE, address(this));
60+
initCalls[1] = B20FactoryLib.encodeGrantRole(B20Constants.BURN_ROLE, address(this));
61+
initCalls[2] = abi.encodeCall(IB20.renounceLastAdmin, ());
62+
63+
bytes memory params = B20FactoryLib.encodeAssetCreateParams(
64+
_WRAPPED_NAME, _WRAPPED_SYMBOL, StdPrecompiles.B20_FACTORY_ADDRESS, _WRAPPED_DECIMALS
65+
);
66+
67+
address wrappedB20_ =
68+
StdPrecompiles.B20_FACTORY.createB20(IB20Factory.B20Variant.ASSET, salt, params, initCalls);
69+
_wrappedB20 = IB20(wrappedB20_);
70+
}
71+
72+
/**
73+
* @notice Returns the Base fcf ERC-20 address wrapped by this contract.
74+
*/
75+
function fcfToken() public pure returns (address) {
76+
return _FCF_TOKEN;
77+
}
78+
79+
/**
80+
* @notice Returns the ERC-20 token that backs the wrapped B20.
81+
*/
82+
function underlying() public view returns (IERC20) {
83+
return _underlying;
84+
}
85+
86+
/**
87+
* @notice Returns the wrapped B20 Asset token minted and burned by this contract.
88+
*/
89+
function wrappedB20() public view returns (IB20) {
90+
return _wrappedB20;
91+
}
92+
93+
/**
94+
* @notice Returns the immutable treasury that receives recovered excess collateral.
95+
*/
96+
function treasury() public view returns (address) {
97+
return _treasury;
98+
}
99+
100+
/**
101+
* @notice Deposits fcf from the caller and mints wrapped B20 to the caller.
102+
*
103+
* Requirements:
104+
*
105+
* - `amount` must be non-zero.
106+
* - The caller must have approved this wrapper to transfer at least `amount` fcf.
107+
*
108+
* Emits an {FCFDeposited} event.
109+
*/
110+
function deposit(uint256 amount) external nonReentrant returns (uint256 minted) {
111+
address caller = _msgSender();
112+
return _deposit(caller, caller, amount);
113+
}
114+
115+
/**
116+
* @notice Deposits fcf from the caller and mints wrapped B20 to `receiver`.
117+
*
118+
* Requirements:
119+
*
120+
* - `receiver` must not be the zero address or this wrapper.
121+
* - `amount` must be non-zero.
122+
* - The caller must have approved this wrapper to transfer at least `amount` fcf.
123+
*
124+
* Emits an {FCFDeposited} event.
125+
*/
126+
function depositFor(address receiver, uint256 amount) external nonReentrant returns (uint256 minted) {
127+
return _deposit(_msgSender(), receiver, amount);
128+
}
129+
130+
/**
131+
* @notice Burns wrapped B20 from the caller and withdraws fcf to the caller.
132+
*
133+
* Requirements:
134+
*
135+
* - `amount` must be non-zero.
136+
* - The caller must have approved this wrapper to transfer at least `amount` wrapped B20.
137+
*
138+
* Emits an {FCFWithdrawn} event.
139+
*/
140+
function withdraw(uint256 amount) external nonReentrant returns (uint256 withdrawn) {
141+
address caller = _msgSender();
142+
return _withdraw(caller, caller, amount);
143+
}
144+
145+
/**
146+
* @notice Burns wrapped B20 from the caller and withdraws fcf to `receiver`.
147+
*
148+
* Requirements:
149+
*
150+
* - `receiver` must not be the zero address or this wrapper.
151+
* - `amount` must be non-zero.
152+
* - The caller must have approved this wrapper to transfer at least `amount` wrapped B20.
153+
*
154+
* Emits an {FCFWithdrawn} event.
155+
*/
156+
function withdrawTo(address receiver, uint256 amount) external nonReentrant returns (uint256 withdrawn) {
157+
return _withdraw(_msgSender(), receiver, amount);
158+
}
159+
160+
/**
161+
* @notice Mints wrapped B20 for excess fcf held by the wrapper to the immutable treasury.
162+
*
163+
* @dev `to` is accepted only when it equals {treasury}; this preserves the OZ `_recover(to)`
164+
* shape while preventing arbitrary recovery destinations.
165+
*
166+
* Requirements:
167+
*
168+
* - The caller must be {treasury}.
169+
* - `to` must equal {treasury}.
170+
*
171+
* Emits an {FCFRecovered} event.
172+
*/
173+
function recover(address to) external nonReentrant returns (uint256 recovered) {
174+
address caller = _msgSender();
175+
address treasury_ = _treasury;
176+
if (caller != treasury_) revert FCFWrapperUnauthorizedRecover(caller, treasury_);
177+
recovered = _recover(to);
178+
emit FCFRecovered(caller, treasury_, recovered);
179+
}
180+
181+
/**
182+
* @dev Pulls fcf from `caller` and mints the received balance delta to `receiver`.
183+
*/
184+
function _deposit(address caller, address receiver, uint256 amount) internal virtual returns (uint256 minted) {
185+
if (receiver == address(0) || receiver == address(this)) revert FCFWrapperInvalidReceiver(receiver);
186+
if (amount == 0) revert FCFWrapperZeroAmount();
187+
188+
IERC20 underlying_ = _underlying;
189+
uint256 balanceBefore = underlying_.balanceOf(address(this));
190+
underlying_.safeTransferFrom(caller, address(this), amount);
191+
uint256 balanceAfter = underlying_.balanceOf(address(this));
192+
if (balanceAfter <= balanceBefore) revert FCFWrapperNoUnderlyingReceived(balanceBefore, balanceAfter);
193+
194+
minted = balanceAfter - balanceBefore;
195+
_wrappedB20.mint(receiver, minted);
196+
emit FCFDeposited(caller, receiver, amount, minted);
197+
}
198+
199+
/**
200+
* @dev Pulls wrapped B20 from `caller`, burns it from this wrapper, and releases fcf to `receiver`.
201+
*/
202+
function _withdraw(address caller, address receiver, uint256 amount) internal virtual returns (uint256 withdrawn) {
203+
if (receiver == address(0) || receiver == address(this)) revert FCFWrapperInvalidReceiver(receiver);
204+
if (amount == 0) revert FCFWrapperZeroAmount();
205+
206+
IERC20(address(_wrappedB20)).safeTransferFrom(caller, address(this), amount);
207+
_wrappedB20.burn(amount);
208+
_underlying.safeTransfer(receiver, amount);
209+
210+
emit FCFWithdrawn(caller, receiver, amount);
211+
return amount;
212+
}
213+
214+
/**
215+
* @dev Mints wrapped B20 to `to` for fcf collateral that is not yet represented by B20 supply.
216+
*/
217+
function _recover(address to) internal virtual returns (uint256 recovered) {
218+
address treasury_ = _treasury;
219+
if (to != treasury_) revert FCFWrapperInvalidRecoverRecipient(to, treasury_);
220+
221+
recovered = _underlying.balanceOf(address(this)) - _wrappedB20.totalSupply();
222+
_wrappedB20.mint(treasury_, recovered);
223+
}
224+
}

0 commit comments

Comments
 (0)