Skip to content
Closed
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
63 changes: 63 additions & 0 deletions src/dogeos/INativeDoge.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// SPDX-License-Identifier: MIT

pragma solidity =0.8.24;

/**
* @title INativeDoge
* @notice ERC-20-shaped interface for "token duality" on the native DOGE asset:
* a token contract whose balances ARE native account balances, following the
* Celo model (CELO/GoldToken), rather than a WETH-style wrapped token with
* deposit/withdraw and storage balances.
*
* @dev Production semantics (what an implementation must guarantee):
* - `balanceOf(a)` equals `a.balance` at all times; there are no storage balances.
* - `transfer`/`transferFrom` move NATIVE balance from `from` to `to` WITHOUT
* executing code on the recipient: no `receive()`/`fallback()` is triggered
* (Celo's transfer precompile behaves the same way). Contracts that rely on
* value-call hooks must not assume duality transfers trigger them.
* - Allowances are ordinary contract storage.
*
* A real implementation CANNOT be written in pure EVM: moving another account's
* native balance requires protocol support. Enabling this on DogeOS needs:
* 1. a native-transfer precompile in l2geth/revm (callable only by the duality
* token contract), with prover/stateless-verifier support;
* 2. a predeploy token contract at a canonical DogeOSPredeploy address that
* calls that precompile;
* 3. genesis integration for the predeploy.
* None of that exists yet - it is protocol work tracked separately. This
* interface and the cheatcode-backed `DualityDogeShim` (src/test/mocks/) exist
* so contracts can be developed and functionally tested against duality
* semantics today. Do NOT use the shim's gas numbers; cheatcodes distort them.
*/
interface INativeDoge {
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);

function name() external view returns (string memory);

function symbol() external view returns (string memory);

function decimals() external view returns (uint8);

/// @notice Total native supply. Implementation-defined; the test shim returns 0.
function totalSupply() external view returns (uint256);

/// @notice The native balance of `account` (identical to `account.balance`).
function balanceOf(address account) external view returns (uint256);

/// @notice Moves `amount` of native DOGE from the caller to `to` without
/// executing recipient code.
function transfer(address to, uint256 amount) external returns (bool);

function allowance(address owner, address spender) external view returns (uint256);

function approve(address spender, uint256 amount) external returns (bool);

/// @notice Moves `amount` of native DOGE from `from` to `to` using the caller's
/// allowance, without executing recipient code.
function transferFrom(
address from,
address to,
uint256 amount
) external returns (bool);
}
130 changes: 130 additions & 0 deletions src/test/dogeos/DualityDogeShim.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
// SPDX-License-Identifier: MIT

pragma solidity =0.8.24;

import {Test} from "forge-std/Test.sol";

import {INativeDoge} from "../../dogeos/INativeDoge.sol";
import {DualityDogeShim} from "../mocks/DualityDogeShim.sol";

/// @dev Recipient with a reverting receive(): duality transfers must NOT execute
/// recipient code, so sending to this contract must still succeed.
contract RevertingReceiver {
receive() external payable {
revert("no thanks");
}
}

contract DualityDogeShimTest is Test {
INativeDoge internal _doge;

address internal _alice = makeAddr("alice");
address internal _bob = makeAddr("bob");

function setUp() public {
_doge = new DualityDogeShim();
vm.deal(_alice, 100 ether);
}

function testBalanceOfIsNativeBalance() external {
assertEq(_doge.balanceOf(_alice), _alice.balance);
assertEq(_doge.balanceOf(_alice), 100 ether);
vm.deal(_bob, 7 ether);
assertEq(_doge.balanceOf(_bob), 7 ether);
}

function testTransferMovesNativeBalance() external {
vm.prank(_alice);
vm.expectEmit(true, true, false, true);
emit INativeDoge.Transfer(_alice, _bob, 30 ether);
assertTrue(_doge.transfer(_bob, 30 ether));

assertEq(_alice.balance, 70 ether);
assertEq(_bob.balance, 30 ether);
}

function testTransferToSelfIsNoop() external {
vm.prank(_alice);
_doge.transfer(_alice, 40 ether);
assertEq(_alice.balance, 100 ether);
}

function testTransferZeroAmount() external {
vm.prank(_alice);
assertTrue(_doge.transfer(_bob, 0));
assertEq(_alice.balance, 100 ether);
assertEq(_bob.balance, 0);
}

function testTransferInsufficientBalanceReverts() external {
vm.prank(_alice);
vm.expectRevert(
abi.encodeWithSelector(DualityDogeShim.ErrorInsufficientBalance.selector, _alice, 100 ether, 101 ether)
);
_doge.transfer(_bob, 101 ether);
}

/// @dev Production duality moves balance without executing recipient code; the
/// shim must match - a reverting receive() cannot block the transfer.
function testTransferDoesNotExecuteRecipientCode() external {
RevertingReceiver receiver = new RevertingReceiver();
vm.prank(_alice);
assertTrue(_doge.transfer(address(receiver), 5 ether));
assertEq(address(receiver).balance, 5 ether);
}

function testApproveAndTransferFrom() external {
vm.prank(_alice);
_doge.approve(_bob, 25 ether);
assertEq(_doge.allowance(_alice, _bob), 25 ether);

vm.prank(_bob);
assertTrue(_doge.transferFrom(_alice, _bob, 10 ether));

assertEq(_alice.balance, 90 ether);
assertEq(_bob.balance, 10 ether);
assertEq(_doge.allowance(_alice, _bob), 15 ether);
}

function testTransferFromInfiniteAllowanceNotDecremented() external {
vm.prank(_alice);
_doge.approve(_bob, type(uint256).max);

vm.prank(_bob);
_doge.transferFrom(_alice, _bob, 10 ether);
assertEq(_doge.allowance(_alice, _bob), type(uint256).max);
}

function testTransferFromInsufficientAllowanceReverts() external {
vm.prank(_alice);
_doge.approve(_bob, 5 ether);

vm.prank(_bob);
vm.expectRevert(
abi.encodeWithSelector(DualityDogeShim.ErrorInsufficientAllowance.selector, _alice, _bob, 5 ether, 6 ether)
);
_doge.transferFrom(_alice, _bob, 6 ether);
}

function testTransferFromInsufficientBalanceReverts() external {
vm.prank(_alice);
_doge.approve(_bob, type(uint256).max);

vm.prank(_bob);
vm.expectRevert(
abi.encodeWithSelector(DualityDogeShim.ErrorInsufficientBalance.selector, _alice, 100 ether, 101 ether)
);
_doge.transferFrom(_alice, _bob, 101 ether);
}

function testFuzz_TransferConservesTotal(uint256 amount) external {
amount = bound(amount, 0, 100 ether);
uint256 totalBefore = _alice.balance + _bob.balance;

vm.prank(_alice);
_doge.transfer(_bob, amount);

assertEq(_alice.balance + _bob.balance, totalBefore);
assertEq(_bob.balance, amount);
}
}
110 changes: 110 additions & 0 deletions src/test/mocks/DualityDogeShim.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// SPDX-License-Identifier: MIT

pragma solidity =0.8.24;

import {Vm} from "forge-std/Vm.sol";

import {INativeDoge} from "../../dogeos/INativeDoge.sol";

/**
* @title DualityDogeShim
* @notice TEST-ONLY functional shim for Celo-style token duality on native DOGE.
* @dev Implements {INativeDoge} inside Foundry tests by moving native balances with
* the `vm.deal` cheatcode - the in-EVM stand-in for the native-transfer
* precompile a real implementation requires (see INativeDoge for the protocol
* work involved).
*
* Faithful to production semantics:
* - `balanceOf` IS the native balance; there are no storage balances.
* - transfers do NOT execute recipient code (no receive()/fallback hooks),
* matching the precompile behavior.
* - allowances are ordinary storage; infinite allowance is not decremented.
*
* NOT faithful: gas. Cheatcode calls have arbitrary gas behavior - never use
* this shim for gas measurements.
*
* Lives under src/test/mocks/ so hardhat (which cannot resolve forge-std) never
* compiles it; it is also excluded from slither/coverage. It cannot work outside
* a Foundry test VM: the cheatcode address only answers there.
*/
contract DualityDogeShim is INativeDoge {
/// @dev Foundry cheatcode VM (`address(uint160(uint256(keccak256("hevm cheat code"))))`).
Vm private constant VM = Vm(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D);

error ErrorInsufficientBalance(address from, uint256 balance, uint256 amount);
error ErrorInsufficientAllowance(address owner, address spender, uint256 allowance, uint256 amount);

mapping(address => mapping(address => uint256)) private _allowances;

function name() external pure returns (string memory) {
return "Dogecoin";
}

function symbol() external pure returns (string memory) {
return "DOGE";
}

function decimals() external pure returns (uint8) {
return 18;
}

/// @inheritdoc INativeDoge
function totalSupply() external pure returns (uint256) {
return 0;
}

/// @inheritdoc INativeDoge
function balanceOf(address account) external view returns (uint256) {
return account.balance;
}

/// @inheritdoc INativeDoge
function transfer(address to, uint256 amount) external returns (bool) {
_move(msg.sender, to, amount);
return true;
}

/// @inheritdoc INativeDoge
function allowance(address owner, address spender) external view returns (uint256) {
return _allowances[owner][spender];
}

/// @inheritdoc INativeDoge
function approve(address spender, uint256 amount) external returns (bool) {
_allowances[msg.sender][spender] = amount;
emit Approval(msg.sender, spender, amount);
return true;
}

/// @inheritdoc INativeDoge
function transferFrom(
address from,
address to,
uint256 amount
) external returns (bool) {
uint256 allowed = _allowances[from][msg.sender];
if (allowed != type(uint256).max) {
if (allowed < amount) {
revert ErrorInsufficientAllowance(from, msg.sender, allowed, amount);
}
_allowances[from][msg.sender] = allowed - amount;
}
_move(from, to, amount);
return true;
}

/// @dev Native balance move via cheatcode; order handles to == from correctly.
function _move(
address from,
address to,
uint256 amount
) private {
uint256 fromBalance = from.balance;
if (fromBalance < amount) {
revert ErrorInsufficientBalance(from, fromBalance, amount);
}
VM.deal(from, fromBalance - amount);
VM.deal(to, to.balance + amount);
emit Transfer(from, to, amount);
}
}
Loading