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
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ The BREAD token v1 was deployed on Polygon PoS and the underlying yield generati
- Since xDAI is native gas token on Gnosis Chain, users don't need both the gas token and DAI in order to onboard.
- Since sDAI is the native yield source for xDAI, it's less variable yield and carries less risk than the Aave market. Yields are also generally much higher!

## Recipient restrictions

BREAD cannot be credited to the token contract's own address. `transfer`, `transferFrom`, both `mint` overloads and `claimYield` revert with `InvalidRecipient()` when the recipient is the token contract, so tokens can't be stranded there by a mistyped or copy-pasted recipient. The check lives in `_update`, so it covers every balance-changing path.

Native xDAI is a different case and is deliberately left alone. BREAD is deployed behind `EIP173ProxyWithReceive`, whose `receive()` accepts plain xDAI and never delegates to the implementation — that is a property of the already-deployed proxy and cannot be changed by upgrading the implementation. Rejecting native transfers outright would also break `burn()`, which is paid out by `wxDai.withdraw()` sending xDAI to the contract with empty calldata. So xDAI sent directly to the token contract is accepted and stays there; `rescueToken` only moves ERC20s. If it ever needs sweeping, a native rescue function can be added in a later implementation upgrade.

## Setup

clone and enter this repo then run:
Expand Down
6 changes: 6 additions & 0 deletions src/Bread.sol
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ contract Bread is
error OnlyClaimers();
error MismatchArray();
error MismatchAmount();
error InvalidRecipient();

IWXDAI public immutable wxDai;
ISXDAI public immutable sexyDai;
Expand Down Expand Up @@ -141,6 +142,11 @@ contract Bread is
if (!success) revert NativeTransferFailed();
}

function _update(address from, address to, uint256 value) internal override {
if (to == address(this)) revert InvalidRecipient();
super._update(from, to, value);
}

function transfer(address recipient, uint256 amount) public override returns (bool) {
super.transfer(recipient, amount);
if (this.delegates(recipient) == address(0)) _delegate(recipient, recipient);
Expand Down
69 changes: 69 additions & 0 deletions test/Bread.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -476,5 +476,74 @@ contract BreadTest is Test {
assertEq(breadToken.delegates(newUser), newUser);
}

/// @dev BREAD credited to the token contract itself is rejected on every path:
/// transfer, transferFrom, both mints and claimYield.
function test_cannot_transfer_to_token_contract() public {
breadToken.mint{value: 1 ether}(address(this));

vm.expectRevert(Bread.InvalidRecipient.selector);
breadToken.transfer(address(breadToken), 1 ether);

assertEq(breadToken.balanceOf(address(this)), 1 ether);
assertEq(breadToken.balanceOf(address(breadToken)), 0);
}

function test_cannot_transferFrom_to_token_contract() public {
breadToken.mint{value: 1 ether}(address(this));
breadToken.approve(address(0x42), 1 ether);

vm.prank(address(0x42));
vm.expectRevert(Bread.InvalidRecipient.selector);
breadToken.transferFrom(address(this), address(breadToken), 1 ether);

assertEq(breadToken.balanceOf(address(this)), 1 ether);
assertEq(breadToken.balanceOf(address(breadToken)), 0);
}

function test_cannot_mint_native_to_token_contract() public {
vm.expectRevert(Bread.InvalidRecipient.selector);
breadToken.mint{value: 1 ether}(address(breadToken));

assertEq(breadToken.balanceOf(address(breadToken)), 0);
}

function test_cannot_mint_wxdai_to_token_contract() public {
vm.startPrank(randomHolder);
wxDai.approve(address(breadToken), 1 ether);
vm.expectRevert(Bread.InvalidRecipient.selector);
breadToken.mint(address(breadToken), 1 ether);
vm.stopPrank();

assertEq(breadToken.balanceOf(address(breadToken)), 0);
}

function test_cannot_claim_yield_to_token_contract() public {
breadToken.mint{value: 1 ether}(address(this));
vm.roll(32661497);
vm.prank(randomHolder);
wxDai.transfer(address(sexyDai), 10000 ether);
vm.roll(32661498);
assertGt(breadToken.yieldAccrued(), 0);

vm.expectRevert(Bread.InvalidRecipient.selector);
breadToken.claimYield(1, address(breadToken));

assertEq(breadToken.balanceOf(address(breadToken)), 0);
}

/// @dev burn() is paid out by wxDai.withdraw() sending xDAI to this contract,
/// so redemptions must keep working alongside the recipient check.
function test_burn_receives_native_after_recipient_check() public {
vm.deal(randomEOA, 1 ether);
vm.startPrank(randomEOA);
breadToken.mint{value: 1 ether}(randomEOA);
uint256 balBefore = randomEOA.balance;
breadToken.burn(1 ether, randomEOA);
vm.stopPrank();

assertEq(randomEOA.balance, balBefore + 1 ether);
assertEq(address(breadToken).balance, 0);
}

receive() external payable {}
}