mirror of https://github.com/vacp2p/minime.git
1410 lines
53 KiB
Solidity
1410 lines
53 KiB
Solidity
// SPDX-License-Identifier: UNLICENSED
|
|
pragma solidity ^0.8.19;
|
|
|
|
import { Test } from "forge-std/Test.sol";
|
|
import { Deploy } from "../script/Deploy.s.sol";
|
|
import { DeploymentConfig } from "../script/DeploymentConfig.s.sol";
|
|
|
|
import { TokenController } from "../contracts/TokenController.sol";
|
|
import { NotAuthorized } from "../contracts/Controlled.sol";
|
|
import {
|
|
TransfersDisabled,
|
|
InvalidDestination,
|
|
NotEnoughBalance,
|
|
NotEnoughAllowance,
|
|
AllowanceAlreadySet,
|
|
ControllerRejected,
|
|
Overflow,
|
|
ParentSnapshotNotReached,
|
|
IERC20
|
|
} from "../contracts/MiniMeBase.sol";
|
|
import { MiniMeToken } from "../contracts/MiniMeToken.sol";
|
|
import { ApproveAndCallFallBack } from "../contracts/ApproveAndCallFallBack.sol";
|
|
|
|
contract MiniMeTokenTest is Test {
|
|
DeploymentConfig internal deploymentConfig;
|
|
MiniMeToken internal minimeToken;
|
|
|
|
address internal deployer;
|
|
|
|
address[] internal accounts;
|
|
|
|
function setUp() public virtual {
|
|
Deploy deployment = new Deploy();
|
|
(deploymentConfig, minimeToken) = deployment.run();
|
|
(deployer,,,,,,) = deploymentConfig.activeNetworkConfig();
|
|
|
|
accounts = new address[](4);
|
|
accounts[0] = makeAddr("account0");
|
|
accounts[1] = makeAddr("account1");
|
|
accounts[2] = makeAddr("account2");
|
|
accounts[3] = makeAddr("account3");
|
|
}
|
|
|
|
function testDeployment() public {
|
|
(, address parentToken, uint256 parentSnapShotBlock, string memory name, uint8 decimals, string memory symbol,)
|
|
= deploymentConfig.activeNetworkConfig();
|
|
vm.pauseGasMetering();
|
|
assertEq(minimeToken.name(), name, "name should be correct");
|
|
assertEq(minimeToken.symbol(), symbol, "symbol should be correct");
|
|
assertEq(minimeToken.decimals(), decimals, "decimals should be correct");
|
|
assertEq(minimeToken.controller(), deployer, "controller should be correct");
|
|
assertEq(address(minimeToken.parentToken()), parentToken, "parent token should be correct");
|
|
assertEq(minimeToken.parentSnapShotBlock(), parentSnapShotBlock, "parent snapshot block should be correct");
|
|
assertEq(minimeToken.totalSupply(), 0, "total supply should be correct");
|
|
vm.resumeGasMetering();
|
|
}
|
|
|
|
function _generateTokens(address to, uint256 amount) internal {
|
|
vm.prank(deployer);
|
|
minimeToken.generateTokens(to, amount);
|
|
}
|
|
}
|
|
|
|
contract AcceptingController is TokenController {
|
|
event Received(address, uint256);
|
|
|
|
receive() external payable {
|
|
emit Received(msg.sender, msg.value);
|
|
}
|
|
|
|
function proxyPayment(address) public payable override returns (bool) {
|
|
return true;
|
|
}
|
|
|
|
function onTransfer(address, address, uint256) public pure override returns (bool) {
|
|
return true;
|
|
}
|
|
|
|
function onApprove(address, address, uint256) public pure override returns (bool) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
contract RejectingController is TokenController {
|
|
function proxyPayment(address) public payable override returns (bool) {
|
|
return false;
|
|
}
|
|
|
|
function onTransfer(address, address, uint256) public pure override returns (bool) {
|
|
return false;
|
|
}
|
|
|
|
function onApprove(address, address, uint256) public pure override returns (bool) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
contract ReceiveTest is MiniMeTokenTest {
|
|
function setUp() public virtual override {
|
|
MiniMeTokenTest.setUp();
|
|
}
|
|
|
|
function testAcceptingEther() public {
|
|
vm.pauseGasMetering();
|
|
AcceptingController acceptingController = new AcceptingController();
|
|
vm.prank(deployer);
|
|
minimeToken.changeController(payable(address(acceptingController)));
|
|
|
|
vm.deal(address(accounts[0]), 10 ether);
|
|
vm.startPrank(address(accounts[0]));
|
|
vm.resumeGasMetering();
|
|
(bool result,) = payable(address(minimeToken)).call{ value: 1 ether }("");
|
|
vm.pauseGasMetering();
|
|
vm.stopPrank();
|
|
|
|
assertTrue(result, "ether transfer should be successful");
|
|
assertEq(address(minimeToken).balance, 0, "minimeToken balance should be correct");
|
|
assertEq(address(acceptingController).balance, 1 ether, "acceptingController balance should be correct");
|
|
assertEq(address(accounts[0]).balance, 9 ether, "account balance should be correct");
|
|
vm.resumeGasMetering();
|
|
}
|
|
|
|
function testRejectingEther() public {
|
|
vm.pauseGasMetering();
|
|
RejectingController rejectingController = new RejectingController();
|
|
vm.prank(deployer);
|
|
minimeToken.changeController(payable(address(rejectingController)));
|
|
|
|
vm.deal(address(accounts[0]), 10 ether);
|
|
vm.startPrank(address(accounts[0]));
|
|
vm.resumeGasMetering();
|
|
(bool result,) = payable(address(minimeToken)).call{ value: 1 ether }("");
|
|
vm.pauseGasMetering();
|
|
vm.stopPrank();
|
|
|
|
assertFalse(result, "ether transfer should be rejected");
|
|
assertEq(address(minimeToken).balance, 0, "minimeToken balance should be correct");
|
|
assertEq(address(rejectingController).balance, 0, "rejectingController balance should be correct");
|
|
assertEq(address(accounts[0]).balance, 10 ether, "account balance should be correct");
|
|
vm.resumeGasMetering();
|
|
}
|
|
}
|
|
|
|
contract GenerateTokensTest is MiniMeTokenTest {
|
|
function setUp() public virtual override {
|
|
MiniMeTokenTest.setUp();
|
|
}
|
|
|
|
function test_RevertWhen_SenderIsNotController() public {
|
|
vm.expectRevert(NotAuthorized.selector);
|
|
minimeToken.generateTokens(accounts[0], 10);
|
|
}
|
|
|
|
function testGenerateTokens() public {
|
|
_generateTokens(accounts[0], 10);
|
|
vm.pauseGasMetering();
|
|
assertEq(minimeToken.totalSupply(), 10, "total supply should be correct");
|
|
assertEq(minimeToken.balanceOf(accounts[0]), 10, "receiver should have balance");
|
|
vm.resumeGasMetering();
|
|
}
|
|
|
|
function testGenerateTokensSupplyOverflow() public {
|
|
vm.pauseGasMetering();
|
|
uint128 max_uint128;
|
|
unchecked {
|
|
max_uint128 = max_uint128 - 1;
|
|
}
|
|
_generateTokens(accounts[0], max_uint128);
|
|
vm.expectRevert(Overflow.selector);
|
|
_generateTokens(accounts[1], 1);
|
|
vm.resumeGasMetering();
|
|
}
|
|
}
|
|
|
|
contract TransferTest is MiniMeTokenTest {
|
|
function setUp() public virtual override {
|
|
MiniMeTokenTest.setUp();
|
|
}
|
|
|
|
function testMultipleTransferToSame() public {
|
|
vm.pauseGasMetering();
|
|
_generateTokens(accounts[0], 10);
|
|
_generateTokens(accounts[1], 10);
|
|
vm.roll(block.number + 1);
|
|
|
|
vm.prank(accounts[0]);
|
|
vm.resumeGasMetering();
|
|
minimeToken.transfer(accounts[2], 2);
|
|
vm.pauseGasMetering();
|
|
|
|
vm.prank(accounts[1]);
|
|
vm.resumeGasMetering();
|
|
minimeToken.transfer(accounts[2], 2);
|
|
vm.pauseGasMetering();
|
|
|
|
assertEq(minimeToken.balanceOf(accounts[0]), 8, "balance of sender 0 should be reduced");
|
|
assertEq(minimeToken.balanceOf(accounts[1]), 8, "balance of sender 1 should be reduced");
|
|
assertEq(minimeToken.balanceOf(accounts[2]), 4, "balance of receiver should be increased");
|
|
|
|
vm.resumeGasMetering();
|
|
}
|
|
|
|
function testMultipleTransferToSame2() public {
|
|
vm.pauseGasMetering();
|
|
_generateTokens(accounts[0], 10);
|
|
_generateTokens(accounts[1], 10);
|
|
_generateTokens(accounts[2], 1);
|
|
vm.roll(block.number + 1);
|
|
|
|
vm.prank(accounts[0]);
|
|
vm.resumeGasMetering();
|
|
minimeToken.transfer(accounts[2], 2);
|
|
vm.pauseGasMetering();
|
|
|
|
vm.prank(accounts[1]);
|
|
vm.resumeGasMetering();
|
|
minimeToken.transfer(accounts[2], 2);
|
|
vm.pauseGasMetering();
|
|
|
|
assertEq(minimeToken.balanceOf(accounts[0]), 8, "balance of sender 0 should be reduced");
|
|
assertEq(minimeToken.balanceOf(accounts[1]), 8, "balance of sender 1 should be reduced");
|
|
assertEq(minimeToken.balanceOf(accounts[2]), 5, "balance of receiver should be increased");
|
|
|
|
vm.resumeGasMetering();
|
|
}
|
|
|
|
function testDoubleTransfer() public {
|
|
vm.pauseGasMetering();
|
|
|
|
_generateTokens(accounts[0], 10);
|
|
vm.roll(block.number + 1);
|
|
vm.startPrank(accounts[0]);
|
|
vm.resumeGasMetering();
|
|
minimeToken.transfer(accounts[1], 2);
|
|
minimeToken.transfer(accounts[1], 2);
|
|
vm.pauseGasMetering();
|
|
vm.stopPrank();
|
|
|
|
assertEq(minimeToken.balanceOf(accounts[0]), 6, "balance of sender should be reduced");
|
|
assertEq(minimeToken.balanceOf(accounts[1]), 4, "balance of receiver should be increased");
|
|
|
|
vm.resumeGasMetering();
|
|
}
|
|
|
|
function testDoubleTransfer2() public {
|
|
vm.pauseGasMetering();
|
|
|
|
_generateTokens(accounts[0], 10);
|
|
_generateTokens(accounts[1], 1);
|
|
vm.roll(block.number + 1);
|
|
vm.startPrank(accounts[0]);
|
|
vm.resumeGasMetering();
|
|
minimeToken.transfer(accounts[1], 2);
|
|
minimeToken.transfer(accounts[1], 2);
|
|
vm.pauseGasMetering();
|
|
vm.stopPrank();
|
|
|
|
assertEq(minimeToken.balanceOf(accounts[0]), 6, "balance of sender should be reduced");
|
|
assertEq(minimeToken.balanceOf(accounts[1]), 5, "balance of receiver should be increased");
|
|
|
|
vm.resumeGasMetering();
|
|
}
|
|
|
|
function testTransfer() public {
|
|
vm.pauseGasMetering();
|
|
uint256 currentBlock = block.number;
|
|
uint256 nextBlock = currentBlock + 1;
|
|
|
|
_generateTokens(accounts[0], 10);
|
|
|
|
// enforce the next block
|
|
vm.roll(nextBlock);
|
|
|
|
vm.prank(accounts[0]);
|
|
|
|
vm.resumeGasMetering();
|
|
minimeToken.transfer(accounts[1], 2);
|
|
vm.pauseGasMetering();
|
|
|
|
assertEq(minimeToken.totalSupply(), 10, "total supply should be correct");
|
|
assertEq(minimeToken.balanceOf(accounts[0]), 8, "balance of sender should be reduced");
|
|
assertEq(minimeToken.balanceOf(accounts[1]), 2, "balance of receiver should be increased");
|
|
|
|
assertEq(minimeToken.balanceOfAt(accounts[0], currentBlock), 10, "balance at original block should be correct");
|
|
vm.resumeGasMetering();
|
|
}
|
|
|
|
function testTransfer2() public {
|
|
vm.pauseGasMetering();
|
|
uint256 currentBlock = block.number;
|
|
uint256 nextBlock = currentBlock + 1;
|
|
|
|
_generateTokens(accounts[0], 10);
|
|
_generateTokens(accounts[1], 1);
|
|
|
|
// enforce the next block
|
|
vm.roll(nextBlock);
|
|
|
|
vm.prank(accounts[0]);
|
|
|
|
vm.resumeGasMetering();
|
|
minimeToken.transfer(accounts[1], 2);
|
|
vm.pauseGasMetering();
|
|
|
|
assertEq(minimeToken.balanceOf(accounts[0]), 8, "balance of sender should be reduced");
|
|
assertEq(minimeToken.balanceOf(accounts[1]), 3, "balance of receiver should be increased");
|
|
|
|
vm.resumeGasMetering();
|
|
}
|
|
|
|
function testInvalidDestinationTransfer() public {
|
|
vm.pauseGasMetering();
|
|
_generateTokens(accounts[0], 10);
|
|
vm.expectRevert(InvalidDestination.selector);
|
|
vm.prank(accounts[0]);
|
|
vm.resumeGasMetering();
|
|
minimeToken.transfer(address(0), 2);
|
|
}
|
|
|
|
function testInvalidDestinationTransfer2() public {
|
|
vm.pauseGasMetering();
|
|
_generateTokens(accounts[0], 10);
|
|
vm.expectRevert(InvalidDestination.selector);
|
|
vm.prank(accounts[0]);
|
|
vm.resumeGasMetering();
|
|
minimeToken.transfer(address(minimeToken), 2);
|
|
}
|
|
|
|
function testTransferDisabled() public {
|
|
vm.pauseGasMetering();
|
|
_generateTokens(accounts[0], 10);
|
|
vm.prank(deployer);
|
|
minimeToken.enableTransfers(false);
|
|
vm.prank(accounts[0]);
|
|
vm.expectRevert(TransfersDisabled.selector);
|
|
vm.resumeGasMetering();
|
|
minimeToken.transfer(accounts[1], 2);
|
|
vm.pauseGasMetering();
|
|
assertEq(minimeToken.balanceOf(accounts[0]), 10, "balance of sender shouldn't be reduced");
|
|
assertEq(minimeToken.balanceOf(accounts[1]), 0, "balance of receiver shouldn't be increased");
|
|
vm.resumeGasMetering();
|
|
}
|
|
|
|
function testTransferFromDisabled() public {
|
|
vm.pauseGasMetering();
|
|
_generateTokens(accounts[0], 10);
|
|
vm.prank(accounts[0]);
|
|
minimeToken.approve(accounts[1], 2);
|
|
vm.prank(deployer);
|
|
minimeToken.enableTransfers(false);
|
|
vm.prank(accounts[1]);
|
|
vm.expectRevert(TransfersDisabled.selector);
|
|
vm.resumeGasMetering();
|
|
minimeToken.transferFrom(accounts[0], accounts[1], 2);
|
|
vm.pauseGasMetering();
|
|
assertEq(minimeToken.balanceOf(accounts[0]), 10, "balance of sender shouldn't be reduced");
|
|
assertEq(minimeToken.balanceOf(accounts[1]), 0, "balance of receiver shouldn't be increased");
|
|
vm.resumeGasMetering();
|
|
}
|
|
|
|
function testTransferNoBalance() public {
|
|
vm.pauseGasMetering();
|
|
vm.prank(accounts[0]);
|
|
vm.expectRevert(NotEnoughBalance.selector);
|
|
vm.resumeGasMetering();
|
|
minimeToken.transfer(accounts[1], 2);
|
|
vm.pauseGasMetering();
|
|
assertEq(minimeToken.balanceOf(accounts[0]), 0, "balance of sender shouldn't be reduced");
|
|
assertEq(minimeToken.balanceOf(accounts[1]), 0, "balance of receiver shouldn't be increased");
|
|
vm.resumeGasMetering();
|
|
}
|
|
|
|
function testRejectedTransfer() public {
|
|
vm.pauseGasMetering();
|
|
address payable rejectingController = payable(address(new RejectingController()));
|
|
_generateTokens(accounts[0], 10);
|
|
vm.prank(deployer);
|
|
minimeToken.changeController(rejectingController);
|
|
vm.prank(accounts[0]);
|
|
vm.expectRevert(ControllerRejected.selector);
|
|
vm.resumeGasMetering();
|
|
minimeToken.transfer(accounts[1], 2);
|
|
vm.pauseGasMetering();
|
|
assertEq(minimeToken.balanceOf(accounts[0]), 10, "balance of sender shouldn't be reduced");
|
|
assertEq(minimeToken.balanceOf(accounts[1]), 0, "balance of receiver shouldn't be increased");
|
|
vm.resumeGasMetering();
|
|
}
|
|
|
|
function testTransferControllerZero() public {
|
|
vm.pauseGasMetering();
|
|
_generateTokens(accounts[0], 10);
|
|
vm.prank(deployer);
|
|
minimeToken.changeController(payable(address(0)));
|
|
vm.prank(accounts[0]);
|
|
vm.resumeGasMetering();
|
|
minimeToken.transfer(accounts[1], 2);
|
|
vm.pauseGasMetering();
|
|
assertEq(minimeToken.balanceOf(accounts[0]), 8, "balance of sender should be reduced");
|
|
assertEq(minimeToken.balanceOf(accounts[1]), 2, "balance of receiver should be increased");
|
|
vm.resumeGasMetering();
|
|
}
|
|
}
|
|
|
|
contract AllowanceTest is MiniMeTokenTest {
|
|
function setUp() public virtual override {
|
|
MiniMeTokenTest.setUp();
|
|
}
|
|
|
|
function testAllowance() public {
|
|
vm.pauseGasMetering();
|
|
vm.prank(accounts[0]);
|
|
vm.resumeGasMetering();
|
|
minimeToken.approve(accounts[1], 2);
|
|
vm.pauseGasMetering();
|
|
uint256 allowed = minimeToken.allowance(accounts[0], accounts[1]);
|
|
assertEq(allowed, 2, "allowance should be correct");
|
|
|
|
uint256 currentBlock = block.number;
|
|
uint256 nextBlock = currentBlock + 1;
|
|
|
|
// ensure `accounts[0]` has tokens
|
|
_generateTokens(accounts[0], 10);
|
|
|
|
// enforce the next block
|
|
vm.roll(nextBlock);
|
|
|
|
vm.prank(accounts[1]);
|
|
minimeToken.transferFrom(accounts[0], accounts[2], 1);
|
|
|
|
allowed = minimeToken.allowance(accounts[0], accounts[1]);
|
|
assertEq(allowed, 1, "allowance should be reduced");
|
|
|
|
assertEq(minimeToken.totalSupply(), 10, "total supply should be correct");
|
|
assertEq(minimeToken.balanceOf(accounts[0]), 9, "balance of sender should be reduced");
|
|
assertEq(minimeToken.balanceOf(accounts[2]), 1, "balance of receiver should be increased");
|
|
|
|
// check balance at blocks
|
|
assertEq(minimeToken.balanceOfAt(accounts[0], currentBlock), 10, "balance at original block should be correct");
|
|
assertEq(minimeToken.balanceOfAt(accounts[0], nextBlock), 9, "balance at next block should be correct");
|
|
assertEq(minimeToken.balanceOfAt(accounts[2], nextBlock), 1, "balance at next block should be correct");
|
|
vm.resumeGasMetering();
|
|
}
|
|
|
|
function testNoAllowance() public {
|
|
vm.pauseGasMetering();
|
|
_generateTokens(accounts[0], 10);
|
|
vm.prank(accounts[1]);
|
|
uint256 allowed = minimeToken.allowance(accounts[0], accounts[1]);
|
|
assertEq(allowed, 0, "allowance should be correct");
|
|
vm.expectRevert(NotEnoughAllowance.selector);
|
|
vm.resumeGasMetering();
|
|
minimeToken.transferFrom(accounts[0], accounts[1], 1);
|
|
}
|
|
|
|
function testApproveTransferDisabled() public {
|
|
vm.pauseGasMetering();
|
|
_generateTokens(accounts[0], 10);
|
|
vm.prank(deployer);
|
|
minimeToken.enableTransfers(false);
|
|
vm.prank(accounts[0]);
|
|
vm.expectRevert(TransfersDisabled.selector);
|
|
vm.resumeGasMetering();
|
|
minimeToken.approve(accounts[1], 2);
|
|
vm.pauseGasMetering();
|
|
assertEq(minimeToken.allowance(accounts[0], accounts[1]), 0, "allowance should be 0");
|
|
vm.resumeGasMetering();
|
|
}
|
|
|
|
function testAllowanceAlreadySet() public {
|
|
vm.pauseGasMetering();
|
|
_generateTokens(accounts[0], 10);
|
|
vm.startPrank(accounts[0]);
|
|
vm.resumeGasMetering();
|
|
minimeToken.approve(accounts[1], 2);
|
|
vm.pauseGasMetering();
|
|
assertEq(minimeToken.allowance(accounts[0], accounts[1]), 2, "allowance should be 2");
|
|
vm.expectRevert(AllowanceAlreadySet.selector);
|
|
vm.resumeGasMetering();
|
|
minimeToken.approve(accounts[1], 3);
|
|
vm.pauseGasMetering();
|
|
vm.stopPrank();
|
|
assertEq(minimeToken.allowance(accounts[0], accounts[1]), 2, "allowance should stay 2");
|
|
vm.resumeGasMetering();
|
|
}
|
|
|
|
function testAllowanceReset() public {
|
|
vm.pauseGasMetering();
|
|
_generateTokens(accounts[0], 10);
|
|
vm.startPrank(accounts[0]);
|
|
vm.resumeGasMetering();
|
|
minimeToken.approve(accounts[1], 2);
|
|
vm.pauseGasMetering();
|
|
assertEq(minimeToken.allowance(accounts[0], accounts[1]), 2, "allowance should be 2");
|
|
vm.resumeGasMetering();
|
|
minimeToken.approve(accounts[1], 0);
|
|
vm.pauseGasMetering();
|
|
assertEq(minimeToken.allowance(accounts[0], accounts[1]), 0, "allowance should be 0");
|
|
vm.resumeGasMetering();
|
|
minimeToken.approve(accounts[1], 3);
|
|
vm.pauseGasMetering();
|
|
vm.stopPrank();
|
|
assertEq(minimeToken.allowance(accounts[0], accounts[1]), 3, "allowance should be 3");
|
|
vm.resumeGasMetering();
|
|
}
|
|
|
|
function testApproveAndCall() public {
|
|
vm.pauseGasMetering();
|
|
_generateTokens(accounts[0], 10);
|
|
ApproverAccount approverAccount = new ApproverAccount(minimeToken);
|
|
vm.startPrank(accounts[0]);
|
|
vm.resumeGasMetering();
|
|
minimeToken.approveAndCall(
|
|
address(approverAccount), 2, abi.encodeWithSelector(approverAccount.depositToken1.selector, "message", 123)
|
|
);
|
|
vm.pauseGasMetering();
|
|
assertEq(minimeToken.allowance(accounts[0], address(approverAccount)), 0, "allowance should be 0");
|
|
assertEq(minimeToken.balanceOf(address(approverAccount)), 2, "approverAccount should have 2 tokens");
|
|
assertEq(minimeToken.balanceOf(accounts[0]), 8, "balance of sender should be reduced");
|
|
assertEq(approverAccount.message(), "message", "message should be correct");
|
|
minimeToken.approveAndCall(
|
|
address(approverAccount), 2, abi.encodeWithSelector(approverAccount.depositToken2.selector, true, "data")
|
|
);
|
|
assertEq(minimeToken.allowance(accounts[0], address(approverAccount)), 0, "allowance should be 0");
|
|
assertEq(minimeToken.balanceOf(address(approverAccount)), 4, "approverAccount should have 2 tokens");
|
|
assertEq(minimeToken.balanceOf(accounts[0]), 6, "balance of sender should be reduced");
|
|
assertEq(approverAccount.data(), "data", "data should be correct");
|
|
vm.expectRevert("ApproverAccount: invalid method");
|
|
minimeToken.approveAndCall(
|
|
address(approverAccount),
|
|
2,
|
|
abi.encodeWithSelector(approverAccount.unsupportedMethod.selector, true, "data")
|
|
);
|
|
vm.stopPrank();
|
|
vm.resumeGasMetering();
|
|
}
|
|
|
|
function testRejectedApproval() public {
|
|
vm.pauseGasMetering();
|
|
address payable rejectingController = payable(address(new RejectingController()));
|
|
_generateTokens(accounts[0], 10);
|
|
vm.prank(deployer);
|
|
minimeToken.changeController(rejectingController);
|
|
vm.prank(accounts[0]);
|
|
vm.expectRevert(ControllerRejected.selector);
|
|
vm.resumeGasMetering();
|
|
minimeToken.approve(accounts[1], 2);
|
|
vm.pauseGasMetering();
|
|
assertEq(minimeToken.allowance(accounts[0], accounts[1]), 0, "allowance should be 0");
|
|
vm.resumeGasMetering();
|
|
}
|
|
}
|
|
|
|
contract ApproverAccount is ApproveAndCallFallBack {
|
|
IERC20 public token;
|
|
|
|
string public message;
|
|
bytes public data;
|
|
|
|
constructor(IERC20 _token) {
|
|
token = _token;
|
|
}
|
|
|
|
event ApprovalReceived(address _from, uint256 _amount, address _token, bytes _data);
|
|
event TokenDeposit1(address indexed _from, uint256 _amount, string _message, uint256 _number);
|
|
event TokenDeposit2(address indexed _from, uint256 _amount, bool _value, bytes _data);
|
|
|
|
function receiveApproval(address _from, uint256 _amount, address _token, bytes memory _data) public override {
|
|
emit ApprovalReceived(_from, _amount, _token, _data);
|
|
require(_token == address(token), "ApproverAccount: token is not correct");
|
|
bytes4 sig = abiDecodeSig(_data);
|
|
bytes memory cdata = slice(_data, 4, _data.length - 4);
|
|
if (sig == this.depositToken1.selector) {
|
|
(string memory _message, uint256 _number) = abi.decode(cdata, (string, uint256));
|
|
depositToken1(_from, _amount, _message, _number);
|
|
} else if (sig == this.depositToken2.selector) {
|
|
(bool _value, bytes memory _decodedData) = abi.decode(cdata, (bool, bytes));
|
|
depositToken2(_from, _amount, _value, _decodedData);
|
|
} else {
|
|
revert("ApproverAccount: invalid method");
|
|
}
|
|
}
|
|
|
|
function depositToken1(string memory _message, uint256 _number) external {
|
|
depositToken1(msg.sender, token.allowance(msg.sender, address(this)), _message, _number);
|
|
}
|
|
|
|
function depositToken2(bool _value, bytes memory _data) external {
|
|
depositToken2(msg.sender, token.allowance(msg.sender, address(this)), _value, _data);
|
|
}
|
|
|
|
function depositToken1(address _from, uint256 _amount, string memory _message, uint256 _number) internal {
|
|
IERC20(token).transferFrom(_from, address(this), _amount);
|
|
message = _message;
|
|
emit TokenDeposit1(_from, _amount, _message, _number);
|
|
}
|
|
|
|
function depositToken2(address _from, uint256 _amount, bool _value, bytes memory _data) internal {
|
|
IERC20(token).transferFrom(_from, address(this), _amount);
|
|
data = _data;
|
|
emit TokenDeposit2(_from, _amount, _value, _data);
|
|
}
|
|
|
|
function unsupportedMethod() external pure {
|
|
revert("ApproverAccount: unsupported method");
|
|
}
|
|
/**
|
|
* @dev decodes sig of abi encoded call
|
|
* @param _data abi encoded data
|
|
* @return sig (first 4 bytes)
|
|
*/
|
|
|
|
function abiDecodeSig(bytes memory _data) private pure returns (bytes4 sig) {
|
|
assembly {
|
|
sig := mload(add(_data, add(0x20, 0)))
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @dev get a slice of byte array
|
|
* @param _bytes source
|
|
* @param _start pointer
|
|
* @param _length size to read
|
|
* @return sliced bytes
|
|
*/
|
|
function slice(bytes memory _bytes, uint256 _start, uint256 _length) private pure returns (bytes memory) {
|
|
require(_bytes.length >= (_start + _length));
|
|
|
|
bytes memory tempBytes;
|
|
|
|
assembly {
|
|
switch iszero(_length)
|
|
case 0 {
|
|
// Get a location of some free memory and store it in tempBytes as
|
|
// Solidity does for memory variables.
|
|
tempBytes := mload(0x40)
|
|
|
|
// The first word of the slice result is potentially a partial
|
|
// word read from the original array. To read it, we calculate
|
|
// the length of that partial word and start copying that many
|
|
// bytes into the array. The first word we copy will start with
|
|
// data we don't care about, but the last `lengthmod` bytes will
|
|
// land at the beginning of the contents of the new array. When
|
|
// we're done copying, we overwrite the full first word with
|
|
// the actual length of the slice.
|
|
let lengthmod := and(_length, 31)
|
|
|
|
// The multiplication in the next line is necessary
|
|
// because when slicing multiples of 32 bytes (lengthmod == 0)
|
|
// the following copy loop was copying the origin's length
|
|
// and then ending prematurely not copying everything it should.
|
|
let mc := add(add(tempBytes, lengthmod), mul(0x20, iszero(lengthmod)))
|
|
let end := add(mc, _length)
|
|
|
|
for {
|
|
// The multiplication in the next line has the same exact purpose
|
|
// as the one above.
|
|
let cc := add(add(add(_bytes, lengthmod), mul(0x20, iszero(lengthmod))), _start)
|
|
} lt(mc, end) {
|
|
mc := add(mc, 0x20)
|
|
cc := add(cc, 0x20)
|
|
} { mstore(mc, mload(cc)) }
|
|
|
|
mstore(tempBytes, _length)
|
|
|
|
//update free-memory pointer
|
|
//allocating the array padded to 32 bytes like the compiler does now
|
|
mstore(0x40, and(add(mc, 31), not(31)))
|
|
}
|
|
//if we want a zero-length slice let's just return a zero-length array
|
|
default {
|
|
tempBytes := mload(0x40)
|
|
|
|
mstore(0x40, add(tempBytes, 0x20))
|
|
}
|
|
}
|
|
|
|
return tempBytes;
|
|
}
|
|
}
|
|
|
|
contract DestroyTokensTest is MiniMeTokenTest {
|
|
function setUp() public virtual override {
|
|
MiniMeTokenTest.setUp();
|
|
}
|
|
|
|
function testDestroyTokens() public {
|
|
vm.pauseGasMetering();
|
|
// ensure `accounts[0]` has tokens
|
|
_generateTokens(accounts[0], 10);
|
|
|
|
vm.prank(deployer);
|
|
vm.resumeGasMetering();
|
|
minimeToken.destroyTokens(accounts[0], 3);
|
|
vm.pauseGasMetering();
|
|
assertEq(minimeToken.totalSupply(), 7, "total supply should be correct");
|
|
assertEq(minimeToken.balanceOf(accounts[0]), 7, "balance of account should be reduced");
|
|
vm.resumeGasMetering();
|
|
}
|
|
|
|
function testDestroyTokensNotEnoughSupply() public {
|
|
vm.pauseGasMetering();
|
|
// ensure `accounts[0]` has tokens
|
|
_generateTokens(accounts[0], 10);
|
|
|
|
vm.prank(deployer);
|
|
vm.resumeGasMetering();
|
|
vm.expectRevert(NotEnoughBalance.selector);
|
|
minimeToken.destroyTokens(accounts[0], 11);
|
|
vm.pauseGasMetering();
|
|
assertEq(minimeToken.totalSupply(), 10, "total supply should be correct");
|
|
assertEq(minimeToken.balanceOf(accounts[0]), 10, "balance of account should be reduced");
|
|
vm.resumeGasMetering();
|
|
}
|
|
|
|
function testDestroyTokensNotEnoughBalance() public {
|
|
vm.pauseGasMetering();
|
|
// ensure `accounts[0]` has tokens
|
|
_generateTokens(accounts[0], 10);
|
|
_generateTokens(accounts[1], 10);
|
|
|
|
vm.expectRevert(NotEnoughBalance.selector);
|
|
vm.prank(deployer);
|
|
vm.resumeGasMetering();
|
|
minimeToken.destroyTokens(accounts[0], 11);
|
|
vm.pauseGasMetering();
|
|
assertEq(minimeToken.totalSupply(), 20, "total supply should be correct");
|
|
assertEq(minimeToken.balanceOf(accounts[0]), 10, "balance of account should be reduced");
|
|
vm.resumeGasMetering();
|
|
}
|
|
}
|
|
|
|
contract CreateCloneTokenTest is MiniMeTokenTest {
|
|
function setUp() public virtual override {
|
|
MiniMeTokenTest.setUp();
|
|
}
|
|
|
|
function _createClone() internal returns (MiniMeToken clone) {
|
|
return new MiniMeToken(
|
|
minimeToken,
|
|
block.number,
|
|
"Clone Token 1",
|
|
18,
|
|
"MMTc",
|
|
true);
|
|
}
|
|
|
|
function testCreateCloneToken() public {
|
|
vm.pauseGasMetering();
|
|
// fund some accounts to later check if cloned token has same balances
|
|
uint256 currentBlock = block.number;
|
|
_generateTokens(accounts[0], 7);
|
|
uint256 nextBlock = block.number + 1;
|
|
vm.roll(nextBlock);
|
|
_generateTokens(accounts[1], 3);
|
|
uint256 secondNextBlock = block.number + 2;
|
|
vm.roll(secondNextBlock);
|
|
_generateTokens(accounts[2], 5);
|
|
vm.resumeGasMetering();
|
|
vm.prank(deployer);
|
|
MiniMeToken clone = _createClone();
|
|
vm.pauseGasMetering();
|
|
assertEq(address(clone.parentToken()), address(minimeToken), "parent token should be correct");
|
|
assertEq(clone.parentSnapShotBlock(), block.number, "parent snapshot block should be correct");
|
|
assertEq(clone.totalSupply(), 15, "total supply should be correct");
|
|
assertEq(clone.balanceOf(accounts[0]), 7, "balance of account 0 should be correct");
|
|
assertEq(clone.balanceOf(accounts[1]), 3, "balance of account 1 should be correct");
|
|
assertEq(clone.balanceOf(accounts[2]), 5, "balance of account 2 should be correct");
|
|
|
|
assertEq(clone.totalSupplyAt(currentBlock), 7, "total supply at current block should be correct");
|
|
assertEq(clone.totalSupplyAt(nextBlock), 10, "total supply at next block should be correct");
|
|
|
|
assertEq(
|
|
clone.balanceOfAt(accounts[0], currentBlock), 7, "balance of account 0 at current block should be correct"
|
|
);
|
|
assertEq(
|
|
clone.balanceOfAt(accounts[1], currentBlock), 0, "balance of account 1 at current block should be correct"
|
|
);
|
|
assertEq(
|
|
clone.balanceOfAt(accounts[2], currentBlock), 0, "balance of account 2 at current block should be correct"
|
|
);
|
|
|
|
assertEq(clone.balanceOfAt(accounts[0], nextBlock), 7, "balance of account 0 at next block should be correct");
|
|
assertEq(clone.balanceOfAt(accounts[1], nextBlock), 3, "balance of account 1 at next block should be correct");
|
|
assertEq(clone.balanceOfAt(accounts[2], nextBlock), 0, "balance of account 2 at next block should be correct");
|
|
|
|
assertEq(
|
|
clone.balanceOfAt(accounts[0], secondNextBlock),
|
|
7,
|
|
"balance of account 0 at second next block should be correct"
|
|
);
|
|
assertEq(
|
|
clone.balanceOfAt(accounts[1], secondNextBlock),
|
|
3,
|
|
"balance of account 1 at second next block should be correct"
|
|
);
|
|
assertEq(
|
|
clone.balanceOfAt(accounts[2], secondNextBlock),
|
|
5,
|
|
"balance of account 2 at second next block should be correct"
|
|
);
|
|
|
|
vm.startPrank(deployer);
|
|
clone.generateTokens(accounts[0], 5);
|
|
clone.generateTokens(accounts[1], 5);
|
|
clone.generateTokens(accounts[2], 5);
|
|
clone.generateTokens(accounts[3], 5);
|
|
assertEq(clone.totalSupply(), 35, "total supply should be correct");
|
|
|
|
uint256 thirdNextBlock = block.number + 3;
|
|
vm.roll(thirdNextBlock);
|
|
|
|
clone.generateTokens(accounts[0], 5);
|
|
clone.generateTokens(accounts[1], 5);
|
|
clone.generateTokens(accounts[2], 5);
|
|
clone.generateTokens(accounts[3], 5);
|
|
|
|
assertEq(clone.totalSupply(), 55, "total supply should be correct");
|
|
assertEq(clone.totalSupplyAt(currentBlock - 1), 0, "total supply at current block-1 should be correct");
|
|
assertEq(clone.totalSupplyAt(currentBlock), 7, "total supply at current block should be correct");
|
|
assertEq(clone.totalSupplyAt(thirdNextBlock), 55, "total supply at thirdNextBlock should be correct");
|
|
|
|
assertEq(
|
|
clone.balanceOfAt(accounts[0], currentBlock - 1),
|
|
0,
|
|
"balance of account 0 at current block-1 should be correct"
|
|
);
|
|
assertEq(
|
|
clone.balanceOfAt(accounts[1], currentBlock - 1),
|
|
0,
|
|
"balance of account 1 at current block-1 should be correct"
|
|
);
|
|
assertEq(
|
|
clone.balanceOfAt(accounts[2], currentBlock - 1),
|
|
0,
|
|
"balance of account 2 at current block-1 should be correct"
|
|
);
|
|
assertEq(
|
|
clone.balanceOfAt(accounts[3], currentBlock - 1),
|
|
0,
|
|
"balance of account 3 at current block-1 should be correct"
|
|
);
|
|
|
|
assertEq(
|
|
clone.balanceOfAt(accounts[0], currentBlock), 7, "balance of account 0 at current block should be correct"
|
|
);
|
|
assertEq(
|
|
clone.balanceOfAt(accounts[1], currentBlock), 0, "balance of account 1 at current block should be correct"
|
|
);
|
|
assertEq(
|
|
clone.balanceOfAt(accounts[2], currentBlock), 0, "balance of account 2 at current block should be correct"
|
|
);
|
|
assertEq(
|
|
clone.balanceOfAt(accounts[3], currentBlock), 0, "balance of account 3 at current block should be correct"
|
|
);
|
|
|
|
assertEq(
|
|
clone.balanceOfAt(accounts[0], thirdNextBlock),
|
|
17,
|
|
"balance of account 0 at third next block should be correct"
|
|
);
|
|
assertEq(
|
|
clone.balanceOfAt(accounts[1], thirdNextBlock),
|
|
13,
|
|
"balance of account 1 at third next block should be correct"
|
|
);
|
|
assertEq(
|
|
clone.balanceOfAt(accounts[2], thirdNextBlock),
|
|
15,
|
|
"balance of account 2 at third next block should be correct"
|
|
);
|
|
assertEq(
|
|
clone.balanceOfAt(accounts[3], thirdNextBlock),
|
|
10,
|
|
"balance of account 3 at third next block should be correct"
|
|
);
|
|
vm.resumeGasMetering();
|
|
}
|
|
|
|
function testGenerateTokens() public {
|
|
vm.pauseGasMetering();
|
|
_generateTokens(accounts[0], 10);
|
|
|
|
vm.prank(deployer);
|
|
MiniMeToken clone = _createClone();
|
|
assertEq(clone.totalSupply(), 10, "total supply should be correct");
|
|
|
|
vm.prank(deployer);
|
|
vm.resumeGasMetering();
|
|
clone.generateTokens(accounts[0], 5);
|
|
assertEq(clone.totalSupply(), 15, "total supply should be correct");
|
|
}
|
|
|
|
function testCloneFutureSnapshot() public {
|
|
vm.pauseGasMetering();
|
|
_generateTokens(accounts[0], 10);
|
|
|
|
vm.prank(accounts[3]);
|
|
MiniMeToken clone = new MiniMeToken(
|
|
minimeToken,
|
|
block.number+1,
|
|
"TestFutureSnapshot",
|
|
18,
|
|
"TST",
|
|
true
|
|
);
|
|
vm.expectRevert(ParentSnapshotNotReached.selector);
|
|
vm.prank(accounts[0]);
|
|
vm.resumeGasMetering();
|
|
clone.transfer(accounts[1], 2);
|
|
vm.pauseGasMetering();
|
|
assertEq(clone.balanceOf(accounts[0]), 10, "balance of account 0 should not change");
|
|
assertEq(clone.balanceOf(accounts[1]), 0, "balance of account 1 should not change");
|
|
vm.roll(block.number + 2);
|
|
vm.prank(accounts[0]);
|
|
vm.resumeGasMetering();
|
|
clone.transfer(accounts[1], 2);
|
|
vm.pauseGasMetering();
|
|
assertEq(clone.balanceOf(accounts[0]), 8, "balance of account 0 should be correct");
|
|
assertEq(clone.balanceOf(accounts[1]), 2, "balance of account 1 should be correct");
|
|
|
|
vm.resumeGasMetering();
|
|
}
|
|
}
|
|
|
|
contract ClaimTokensTest is MiniMeTokenTest {
|
|
function setUp() public virtual override {
|
|
MiniMeTokenTest.setUp();
|
|
}
|
|
|
|
function testClaimERC20() public {
|
|
vm.pauseGasMetering();
|
|
vm.startPrank(deployer);
|
|
MiniMeToken claimTest = new MiniMeToken(
|
|
MiniMeToken(payable(address(0))),
|
|
0,
|
|
"TestClaim",
|
|
18,
|
|
"TST",
|
|
true
|
|
);
|
|
claimTest.generateTokens(address(minimeToken), 1234);
|
|
|
|
assertEq(claimTest.balanceOf(address(minimeToken)), 1234, "claimTest minimeToken balance should be correct");
|
|
assertEq(claimTest.balanceOf(address(deployer)), 0, "claimTest deployer balance should be correct");
|
|
|
|
vm.resumeGasMetering();
|
|
minimeToken.claimTokens(claimTest);
|
|
vm.pauseGasMetering();
|
|
|
|
vm.stopPrank();
|
|
|
|
assertEq(claimTest.balanceOf(address(minimeToken)), 0, "claimTest minimeToken balance should be correct");
|
|
assertEq(claimTest.balanceOf(address(deployer)), 1234, "claimTest deployer balance should be correct");
|
|
vm.resumeGasMetering();
|
|
}
|
|
|
|
function testClaimETH() public {
|
|
vm.pauseGasMetering();
|
|
vm.startPrank(deployer);
|
|
vm.deal(address(minimeToken), 1234);
|
|
assertEq(address(minimeToken).balance, 1234, "minimeToken balance should be correct");
|
|
assertEq(address(deployer).balance, 0, "deployer balance should be correct");
|
|
|
|
vm.resumeGasMetering();
|
|
minimeToken.claimTokens(IERC20(address(0)));
|
|
vm.pauseGasMetering();
|
|
|
|
assertEq(address(minimeToken).balance, 0, "minimeToken balance should be correct");
|
|
assertEq(address(deployer).balance, 1234, "deployer balance should be correct");
|
|
|
|
vm.stopPrank();
|
|
vm.resumeGasMetering();
|
|
}
|
|
|
|
function testClaimSelf() public {
|
|
vm.pauseGasMetering();
|
|
vm.startPrank(deployer);
|
|
minimeToken.generateTokens(address(minimeToken), 1234);
|
|
assertEq(minimeToken.balanceOf(address(minimeToken)), 1234, "minimeToken minimeToken balance should be 1234");
|
|
assertEq(minimeToken.balanceOf(address(deployer)), 0, "minimeToken deployer balance should be 0");
|
|
|
|
vm.resumeGasMetering();
|
|
minimeToken.claimTokens(minimeToken);
|
|
vm.pauseGasMetering();
|
|
|
|
assertEq(minimeToken.balanceOf(address(minimeToken)), 0, "minimeToken minimeToken balance should be 0");
|
|
assertEq(minimeToken.balanceOf(address(deployer)), 1234, "minimeToken deployer balance should be 1234");
|
|
vm.stopPrank();
|
|
vm.resumeGasMetering();
|
|
}
|
|
}
|
|
|
|
contract TestSnapshotReads is MiniMeTokenTest {
|
|
function setUp() public virtual override {
|
|
MiniMeTokenTest.setUp();
|
|
}
|
|
|
|
function testSnapshotReads() public {
|
|
assertEq(minimeToken.totalSupply(), 0, "initial total supply should be correct");
|
|
assertEq(minimeToken.balanceOf(accounts[0]), 0, "initial balance of account 0 should be correct");
|
|
assertEq(minimeToken.balanceOf(accounts[1]), 0, "initial balance of account 1 should be correct");
|
|
assertEq(minimeToken.balanceOf(accounts[2]), 0, "initial balance of account 2 should be correct");
|
|
assertEq(minimeToken.balanceOf(accounts[3]), 0, "initial balance of account 3 should be correct");
|
|
|
|
_generateTokens(accounts[0], 10);
|
|
_generateTokens(accounts[1], 5);
|
|
_generateTokens(accounts[2], 3);
|
|
_generateTokens(accounts[3], 1);
|
|
|
|
uint256 currentBlock = block.number;
|
|
uint256 nextBlock = currentBlock + 1;
|
|
uint256 secondNextBlock = currentBlock + 2;
|
|
uint256 thirdNextBlock = currentBlock + 3;
|
|
|
|
vm.roll(nextBlock);
|
|
_generateTokens(accounts[0], 2);
|
|
_generateTokens(accounts[1], 1);
|
|
_generateTokens(accounts[2], 1);
|
|
_generateTokens(accounts[3], 1);
|
|
|
|
vm.roll(secondNextBlock);
|
|
_generateTokens(accounts[0], 1);
|
|
_generateTokens(accounts[1], 1);
|
|
_generateTokens(accounts[2], 1);
|
|
|
|
vm.roll(thirdNextBlock);
|
|
_generateTokens(accounts[0], 1);
|
|
_generateTokens(accounts[1], 1);
|
|
|
|
assertEq(minimeToken.totalSupply(), 29, "total supply should be correct");
|
|
assertEq(minimeToken.balanceOf(accounts[0]), 14, "balance of account 0 should be correct");
|
|
assertEq(minimeToken.balanceOf(accounts[1]), 8, "balance of account 1 should be correct");
|
|
assertEq(minimeToken.balanceOf(accounts[2]), 5, "balance of account 2 should be correct");
|
|
assertEq(minimeToken.balanceOf(accounts[3]), 2, "balance of account 3 should be correct");
|
|
|
|
assertEq(minimeToken.totalSupplyAt(currentBlock), 19, "total supply at current block should be correct");
|
|
assertEq(minimeToken.totalSupplyAt(nextBlock), 24, "total supply at next block should be correct");
|
|
assertEq(minimeToken.totalSupplyAt(secondNextBlock), 27, "total supply at second next block should be correct");
|
|
assertEq(minimeToken.totalSupplyAt(thirdNextBlock), 29, "total supply at third next block should be correct");
|
|
|
|
assertEq(
|
|
minimeToken.balanceOfAt(accounts[0], currentBlock),
|
|
10,
|
|
"balance of account 0 at current block should be correct"
|
|
);
|
|
assertEq(
|
|
minimeToken.balanceOfAt(accounts[1], currentBlock),
|
|
5,
|
|
"balance of account 1 at current block should be correct"
|
|
);
|
|
assertEq(
|
|
minimeToken.balanceOfAt(accounts[2], currentBlock),
|
|
3,
|
|
"balance of account 2 at current block should be correct"
|
|
);
|
|
assertEq(
|
|
minimeToken.balanceOfAt(accounts[3], currentBlock),
|
|
1,
|
|
"balance of account 3 at current block should be correct"
|
|
);
|
|
|
|
assertEq(
|
|
minimeToken.balanceOfAt(accounts[0], nextBlock), 12, "balance of account 0 at next block should be correct"
|
|
);
|
|
assertEq(
|
|
minimeToken.balanceOfAt(accounts[1], nextBlock), 6, "balance of account 1 at next block should be correct"
|
|
);
|
|
assertEq(
|
|
minimeToken.balanceOfAt(accounts[2], nextBlock), 4, "balance of account 2 at next block should be correct"
|
|
);
|
|
assertEq(
|
|
minimeToken.balanceOfAt(accounts[3], nextBlock), 2, "balance of account 3 at next block should be correct"
|
|
);
|
|
|
|
assertEq(
|
|
minimeToken.balanceOfAt(accounts[0], secondNextBlock),
|
|
13,
|
|
"balance of account 0 at second next block should be correct"
|
|
);
|
|
assertEq(
|
|
minimeToken.balanceOfAt(accounts[1], secondNextBlock),
|
|
7,
|
|
"balance of account 1 at second next block should be correct"
|
|
);
|
|
assertEq(
|
|
minimeToken.balanceOfAt(accounts[2], secondNextBlock),
|
|
5,
|
|
"balance of account 2 at second next block should be correct"
|
|
);
|
|
assertEq(
|
|
minimeToken.balanceOfAt(accounts[3], secondNextBlock),
|
|
2,
|
|
"balance of account 3 at second next block should be correct"
|
|
);
|
|
|
|
assertEq(
|
|
minimeToken.balanceOfAt(accounts[0], thirdNextBlock),
|
|
14,
|
|
"balance of account 0 at third next block should be correct"
|
|
);
|
|
assertEq(
|
|
minimeToken.balanceOfAt(accounts[1], thirdNextBlock),
|
|
8,
|
|
"balance of account 1 at third next block should be correct"
|
|
);
|
|
assertEq(
|
|
minimeToken.balanceOfAt(accounts[2], thirdNextBlock),
|
|
5,
|
|
"balance of account 2 at third next block should be correct"
|
|
);
|
|
assertEq(
|
|
minimeToken.balanceOfAt(accounts[3], thirdNextBlock),
|
|
2,
|
|
"balance of account 3 at third next block should be correct"
|
|
);
|
|
|
|
assertEq(
|
|
minimeToken.balanceOfAt(accounts[0], currentBlock - 1),
|
|
0,
|
|
"balance of account 0 at previous block should be correct"
|
|
);
|
|
assertEq(
|
|
minimeToken.balanceOfAt(accounts[1], currentBlock - 1),
|
|
0,
|
|
"balance of account 1 at previous block should be correct"
|
|
);
|
|
assertEq(
|
|
minimeToken.balanceOfAt(accounts[2], currentBlock - 1),
|
|
0,
|
|
"balance of account 2 at previous block should be correct"
|
|
);
|
|
assertEq(
|
|
minimeToken.balanceOfAt(accounts[3], currentBlock - 1),
|
|
0,
|
|
"balance of account 3 at previous block should be correct"
|
|
);
|
|
|
|
assertEq(
|
|
minimeToken.balanceOfAt(accounts[0], thirdNextBlock + 1),
|
|
14,
|
|
"balance of account 0 at next block should be correct"
|
|
);
|
|
assertEq(
|
|
minimeToken.balanceOfAt(accounts[1], thirdNextBlock + 1),
|
|
8,
|
|
"balance of account 1 at next block should be correct"
|
|
);
|
|
assertEq(
|
|
minimeToken.balanceOfAt(accounts[2], thirdNextBlock + 1),
|
|
5,
|
|
"balance of account 2 at next block should be correct"
|
|
);
|
|
assertEq(
|
|
minimeToken.balanceOfAt(accounts[3], thirdNextBlock + 1),
|
|
2,
|
|
"balance of account 3 at next block should be correct"
|
|
);
|
|
}
|
|
}
|
|
|
|
import {
|
|
ERC2612InvalidSigner,
|
|
ERC2612ExpiredSignature,
|
|
ECDSA,
|
|
NotEnoughBalance,
|
|
NotEnoughAllowance
|
|
} from "../contracts/MiniMeBase.sol";
|
|
|
|
contract TestPermit is MiniMeTokenTest {
|
|
SigUtils internal sigUtils;
|
|
uint256 internal ownerPrivateKey = 0xA11CE;
|
|
uint256 internal spenderPrivateKey = 0xB0B;
|
|
address internal owner = vm.addr(ownerPrivateKey);
|
|
address internal spender = vm.addr(spenderPrivateKey);
|
|
|
|
function setUp() public virtual override {
|
|
MiniMeTokenTest.setUp();
|
|
|
|
sigUtils = new SigUtils(minimeToken.DOMAIN_SEPARATOR());
|
|
}
|
|
|
|
function testPermit() public {
|
|
SigUtils.Permit memory permit = SigUtils.Permit({
|
|
owner: owner,
|
|
spender: spender,
|
|
value: 1e18,
|
|
nonce: 0,
|
|
deadline: block.timestamp + 1 days
|
|
});
|
|
|
|
bytes32 digest = sigUtils.getTypedDataHash(permit);
|
|
|
|
(uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, digest);
|
|
|
|
minimeToken.permit(permit.owner, permit.spender, permit.value, permit.deadline, v, r, s);
|
|
|
|
assertEq(minimeToken.allowance(owner, spender), 1e18);
|
|
assertEq(minimeToken.nonces(owner), 1);
|
|
}
|
|
|
|
function testRevertExpiredPermit() public {
|
|
SigUtils.Permit memory permit = SigUtils.Permit({
|
|
owner: owner,
|
|
spender: spender,
|
|
value: 1e18,
|
|
nonce: minimeToken.nonces(owner),
|
|
deadline: block.timestamp - 1 seconds
|
|
});
|
|
|
|
bytes32 digest = sigUtils.getTypedDataHash(permit);
|
|
|
|
(uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, digest);
|
|
|
|
vm.expectRevert(abi.encodeWithSelector(ERC2612ExpiredSignature.selector, permit.deadline));
|
|
minimeToken.permit(permit.owner, permit.spender, permit.value, permit.deadline, v, r, s);
|
|
}
|
|
|
|
function testRevertInvalidSigner() public {
|
|
SigUtils.Permit memory permit = SigUtils.Permit({
|
|
owner: owner,
|
|
spender: spender,
|
|
value: 1e18,
|
|
nonce: minimeToken.nonces(owner),
|
|
deadline: block.timestamp + 1 days
|
|
});
|
|
|
|
bytes32 digest = sigUtils.getTypedDataHash(permit);
|
|
|
|
(uint8 v, bytes32 r, bytes32 s) = vm.sign(spenderPrivateKey, digest); // spender signs owner's approval
|
|
|
|
vm.expectRevert(
|
|
abi.encodeWithSelector(ERC2612InvalidSigner.selector, ECDSA.recover(digest, v, r, s), permit.owner)
|
|
);
|
|
minimeToken.permit(permit.owner, permit.spender, permit.value, permit.deadline, v, r, s);
|
|
}
|
|
|
|
function testRevertInvalidNonce() public {
|
|
SigUtils.Permit memory permit = SigUtils.Permit({
|
|
owner: owner,
|
|
spender: spender,
|
|
value: 1e18,
|
|
nonce: 1, // owner nonce stored on-chain is 0
|
|
deadline: block.timestamp + 1 days
|
|
});
|
|
bytes32 digest = sigUtils.getTypedDataHash(permit);
|
|
(uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, digest);
|
|
|
|
SigUtils.Permit memory permitNeeded = SigUtils.Permit({
|
|
owner: owner,
|
|
spender: spender,
|
|
value: 1e18,
|
|
nonce: 0, // owner nonce stored on-chain is 0
|
|
deadline: block.timestamp + 1 days
|
|
});
|
|
bytes32 digestNeeded = sigUtils.getTypedDataHash(permitNeeded);
|
|
|
|
vm.expectRevert(
|
|
abi.encodeWithSelector(ERC2612InvalidSigner.selector, ECDSA.recover(digestNeeded, v, r, s), permit.owner)
|
|
);
|
|
minimeToken.permit(permit.owner, permit.spender, permit.value, permit.deadline, v, r, s);
|
|
}
|
|
|
|
function testRevertSignatureReplay() public {
|
|
SigUtils.Permit memory permit = SigUtils.Permit({
|
|
owner: owner,
|
|
spender: spender,
|
|
value: 1e18,
|
|
nonce: minimeToken.nonces(owner),
|
|
deadline: block.timestamp + 1 days
|
|
});
|
|
|
|
bytes32 digest = sigUtils.getTypedDataHash(permit);
|
|
(uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, digest);
|
|
|
|
minimeToken.permit(permit.owner, permit.spender, permit.value, permit.deadline, v, r, s);
|
|
|
|
SigUtils.Permit memory permitNeeded = SigUtils.Permit({
|
|
owner: owner,
|
|
spender: spender,
|
|
value: 1e18,
|
|
nonce: minimeToken.nonces(owner),
|
|
deadline: block.timestamp + 1 days
|
|
});
|
|
|
|
bytes32 digestNeeded = sigUtils.getTypedDataHash(permitNeeded);
|
|
vm.expectRevert(
|
|
abi.encodeWithSelector(ERC2612InvalidSigner.selector, ECDSA.recover(digestNeeded, v, r, s), permit.owner)
|
|
);
|
|
minimeToken.permit(permit.owner, permit.spender, permit.value, permit.deadline, v, r, s);
|
|
}
|
|
|
|
function testTransferFromLimitedPermit() public {
|
|
_generateTokens(owner, 1e18);
|
|
SigUtils.Permit memory permit = SigUtils.Permit({
|
|
owner: owner,
|
|
spender: spender,
|
|
value: 1e18,
|
|
nonce: 0,
|
|
deadline: block.timestamp + 1 days
|
|
});
|
|
|
|
bytes32 digest = sigUtils.getTypedDataHash(permit);
|
|
|
|
(uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, digest);
|
|
|
|
minimeToken.permit(permit.owner, permit.spender, permit.value, permit.deadline, v, r, s);
|
|
|
|
vm.prank(spender);
|
|
minimeToken.transferFrom(owner, spender, 1e18);
|
|
|
|
assertEq(minimeToken.balanceOf(owner), 0);
|
|
assertEq(minimeToken.balanceOf(spender), 1e18);
|
|
assertEq(minimeToken.allowance(owner, spender), 0);
|
|
}
|
|
|
|
function testTransferFromMaxPermit() public {
|
|
_generateTokens(owner, 1e18);
|
|
SigUtils.Permit memory permit = SigUtils.Permit({
|
|
owner: owner,
|
|
spender: spender,
|
|
value: type(uint256).max,
|
|
nonce: 0,
|
|
deadline: block.timestamp + 1 days
|
|
});
|
|
|
|
bytes32 digest = sigUtils.getTypedDataHash(permit);
|
|
|
|
(uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, digest);
|
|
|
|
minimeToken.permit(permit.owner, permit.spender, permit.value, permit.deadline, v, r, s);
|
|
|
|
vm.prank(spender);
|
|
minimeToken.transferFrom(owner, spender, 1e18);
|
|
|
|
assertEq(minimeToken.balanceOf(owner), 0);
|
|
assertEq(minimeToken.balanceOf(spender), 1e18);
|
|
assertEq(minimeToken.allowance(owner, spender), type(uint256).max - 1e18);
|
|
}
|
|
|
|
function testInvalidAllowance() public {
|
|
SigUtils.Permit memory permit = SigUtils.Permit({
|
|
owner: owner,
|
|
spender: spender,
|
|
value: 5e17, // approve only 0.5 tokens
|
|
nonce: 0,
|
|
deadline: block.timestamp + 1 days
|
|
});
|
|
|
|
bytes32 digest = sigUtils.getTypedDataHash(permit);
|
|
|
|
(uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, digest);
|
|
|
|
minimeToken.permit(permit.owner, permit.spender, permit.value, permit.deadline, v, r, s);
|
|
|
|
vm.prank(spender);
|
|
vm.expectRevert(NotEnoughAllowance.selector);
|
|
minimeToken.transferFrom(owner, spender, 1e18); // attempt to transfer 1 token
|
|
}
|
|
|
|
function testInvalidBalance() public {
|
|
SigUtils.Permit memory permit = SigUtils.Permit({
|
|
owner: owner,
|
|
spender: spender,
|
|
value: 2e18, // approve 2 tokens
|
|
nonce: 0,
|
|
deadline: block.timestamp + 1 days
|
|
});
|
|
|
|
bytes32 digest = sigUtils.getTypedDataHash(permit);
|
|
|
|
(uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, digest);
|
|
|
|
minimeToken.permit(permit.owner, permit.spender, permit.value, permit.deadline, v, r, s);
|
|
vm.prank(spender);
|
|
vm.expectRevert(NotEnoughBalance.selector);
|
|
minimeToken.transferFrom(owner, spender, 2e18); // attempt to transfer 2 tokens (owner only owns 1)
|
|
}
|
|
}
|
|
|
|
contract SigUtils {
|
|
bytes32 internal DOMAIN_SEPARATOR;
|
|
|
|
constructor(bytes32 _DOMAIN_SEPARATOR) {
|
|
DOMAIN_SEPARATOR = _DOMAIN_SEPARATOR;
|
|
}
|
|
|
|
// keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)");
|
|
bytes32 public constant PERMIT_TYPEHASH = 0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9;
|
|
|
|
struct Permit {
|
|
address owner;
|
|
address spender;
|
|
uint256 value;
|
|
uint256 nonce;
|
|
uint256 deadline;
|
|
}
|
|
|
|
// computes the hash of a permit
|
|
function getStructHash(Permit memory _permit) internal pure returns (bytes32) {
|
|
return keccak256(
|
|
abi.encode(PERMIT_TYPEHASH, _permit.owner, _permit.spender, _permit.value, _permit.nonce, _permit.deadline)
|
|
);
|
|
}
|
|
|
|
// computes the hash of the fully encoded EIP-712 message for the domain, which can be used to recover the signer
|
|
function getTypedDataHash(Permit memory _permit) public view returns (bytes32) {
|
|
return ECDSA.toTypedDataHash(DOMAIN_SEPARATOR, getStructHash(_permit));
|
|
}
|
|
}
|