- CCIP/trustless bridge contracts, GRU tokens, DEX/PMM tests, reserve vault. - Token-aggregation service routes, planner, chain config, relay env templates. - Config snapshots and multi-chain deployment markdown updates. - gitignore services/btc-intake/dist/ (tsc output); do not track dist. Run forge build && forge test before deploy (large solc graph). Made-with: Cursor
386 lines
13 KiB
Solidity
386 lines
13 KiB
Solidity
// SPDX-License-Identifier: MIT
|
|
pragma solidity ^0.8.20;
|
|
|
|
import {Test} from "forge-std/Test.sol";
|
|
import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
|
|
import {CompliantFiatTokenV2} from "../../contracts/tokens/CompliantFiatTokenV2.sol";
|
|
|
|
contract CompliantFiatTokenV2Test is Test {
|
|
using ECDSA for bytes32;
|
|
|
|
bytes32 private constant PERMIT_TYPEHASH =
|
|
keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)");
|
|
bytes32 private constant TRANSFER_WITH_AUTHORIZATION_TYPEHASH =
|
|
keccak256(
|
|
"TransferWithAuthorization(address from,address to,uint256 value,uint256 validAfter,uint256 validBefore,bytes32 nonce)"
|
|
);
|
|
bytes32 private constant RECEIVE_WITH_AUTHORIZATION_TYPEHASH =
|
|
keccak256(
|
|
"ReceiveWithAuthorization(address from,address to,uint256 value,uint256 validAfter,uint256 validBefore,bytes32 nonce)"
|
|
);
|
|
bytes32 private constant CANCEL_AUTHORIZATION_TYPEHASH =
|
|
keccak256("CancelAuthorization(address authorizer,bytes32 nonce)");
|
|
|
|
event CompliantOperationDeclared(
|
|
bytes32 indexed operationType,
|
|
address indexed initiator,
|
|
address indexed authorizer,
|
|
address executor,
|
|
address from,
|
|
address to,
|
|
uint256 value,
|
|
bytes32 reasonHash,
|
|
bytes32 accountingRef,
|
|
bytes32 messageCorrelationId,
|
|
bytes32 legalReferenceHash
|
|
);
|
|
|
|
CompliantFiatTokenV2 internal token;
|
|
|
|
uint256 internal ownerPk;
|
|
uint256 internal adminPk;
|
|
address internal owner;
|
|
address internal admin;
|
|
address internal spender;
|
|
address internal recipient;
|
|
address internal burner;
|
|
address internal governanceExecutor;
|
|
|
|
function setUp() public {
|
|
ownerPk = 0xA11CE;
|
|
adminPk = 0xB0B;
|
|
owner = vm.addr(ownerPk);
|
|
admin = vm.addr(adminPk);
|
|
spender = address(0xCAFE);
|
|
recipient = address(0xBEEF);
|
|
burner = address(0xD00D);
|
|
governanceExecutor = address(0x7777);
|
|
|
|
token = new CompliantFiatTokenV2(
|
|
"Euro Coin (Compliant V2)",
|
|
"cEURC",
|
|
6,
|
|
"EUR",
|
|
"2",
|
|
owner,
|
|
admin,
|
|
1_000_000 * 10 ** 6,
|
|
true
|
|
);
|
|
|
|
bytes32 burnerRole = token.BURNER_ROLE();
|
|
vm.prank(admin);
|
|
token.grantRole(burnerRole, burner);
|
|
vm.prank(admin);
|
|
token.setGovernanceController(governanceExecutor);
|
|
}
|
|
|
|
function testPermitWorks() public {
|
|
uint256 value = 25_000 * 10 ** 6;
|
|
uint256 deadline = block.timestamp + 1 days;
|
|
|
|
bytes32 structHash = keccak256(
|
|
abi.encode(PERMIT_TYPEHASH, owner, spender, value, token.nonces(owner), deadline)
|
|
);
|
|
bytes32 digest = keccak256(
|
|
abi.encodePacked("\x19\x01", token.DOMAIN_SEPARATOR(), structHash)
|
|
);
|
|
(uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPk, digest);
|
|
|
|
token.permit(owner, spender, value, deadline, v, r, s);
|
|
|
|
assertEq(token.allowance(owner, spender), value);
|
|
assertEq(token.nonces(owner), 1);
|
|
}
|
|
|
|
function testTransferWithAuthorizationWorks() public {
|
|
uint256 value = 5_000 * 10 ** 6;
|
|
uint256 validAfter = block.timestamp - 1;
|
|
uint256 validBefore = block.timestamp + 1 days;
|
|
bytes32 nonce = keccak256("auth-1");
|
|
|
|
bytes32 structHash = keccak256(
|
|
abi.encode(
|
|
TRANSFER_WITH_AUTHORIZATION_TYPEHASH,
|
|
owner,
|
|
recipient,
|
|
value,
|
|
validAfter,
|
|
validBefore,
|
|
nonce
|
|
)
|
|
);
|
|
bytes32 digest = keccak256(
|
|
abi.encodePacked("\x19\x01", token.DOMAIN_SEPARATOR(), structHash)
|
|
);
|
|
(uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPk, digest);
|
|
|
|
vm.prank(spender);
|
|
token.transferWithAuthorization(owner, recipient, value, validAfter, validBefore, nonce, v, r, s);
|
|
|
|
assertEq(token.balanceOf(recipient), value);
|
|
assertEq(token.balanceOf(owner), 1_000_000 * 10 ** 6 - value);
|
|
assertTrue(token.authorizationState(owner, nonce));
|
|
}
|
|
|
|
function testReceiveWithAuthorizationRequiresPayeeCallerAndWorks() public {
|
|
uint256 value = 7_500 * 10 ** 6;
|
|
uint256 validAfter = block.timestamp - 1;
|
|
uint256 validBefore = block.timestamp + 1 days;
|
|
bytes32 nonce = keccak256("receive-auth-1");
|
|
|
|
bytes32 structHash = keccak256(
|
|
abi.encode(
|
|
RECEIVE_WITH_AUTHORIZATION_TYPEHASH,
|
|
owner,
|
|
recipient,
|
|
value,
|
|
validAfter,
|
|
validBefore,
|
|
nonce
|
|
)
|
|
);
|
|
bytes32 digest = keccak256(
|
|
abi.encodePacked("\x19\x01", token.DOMAIN_SEPARATOR(), structHash)
|
|
);
|
|
(uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPk, digest);
|
|
|
|
vm.prank(spender);
|
|
vm.expectRevert(
|
|
abi.encodeWithSelector(
|
|
CompliantFiatTokenV2.AuthorizationMustBeUsedByPayee.selector,
|
|
recipient,
|
|
spender
|
|
)
|
|
);
|
|
token.receiveWithAuthorization(owner, recipient, value, validAfter, validBefore, nonce, v, r, s);
|
|
|
|
vm.prank(recipient);
|
|
token.receiveWithAuthorization(owner, recipient, value, validAfter, validBefore, nonce, v, r, s);
|
|
|
|
assertEq(token.balanceOf(recipient), value);
|
|
assertTrue(token.authorizationState(owner, nonce));
|
|
}
|
|
|
|
function testCancelAuthorizationBlocksFutureUse() public {
|
|
uint256 value = 1_000 * 10 ** 6;
|
|
uint256 validAfter = block.timestamp - 1;
|
|
uint256 validBefore = block.timestamp + 1 days;
|
|
bytes32 nonce = keccak256("cancel-auth-1");
|
|
|
|
bytes32 cancelStructHash = keccak256(
|
|
abi.encode(CANCEL_AUTHORIZATION_TYPEHASH, owner, nonce)
|
|
);
|
|
bytes32 cancelDigest = keccak256(
|
|
abi.encodePacked("\x19\x01", token.DOMAIN_SEPARATOR(), cancelStructHash)
|
|
);
|
|
(uint8 cancelV, bytes32 cancelR, bytes32 cancelS) = vm.sign(ownerPk, cancelDigest);
|
|
|
|
token.cancelAuthorization(owner, nonce, cancelV, cancelR, cancelS);
|
|
assertTrue(token.authorizationState(owner, nonce));
|
|
|
|
bytes32 transferStructHash = keccak256(
|
|
abi.encode(
|
|
TRANSFER_WITH_AUTHORIZATION_TYPEHASH,
|
|
owner,
|
|
recipient,
|
|
value,
|
|
validAfter,
|
|
validBefore,
|
|
nonce
|
|
)
|
|
);
|
|
bytes32 transferDigest = keccak256(
|
|
abi.encodePacked("\x19\x01", token.DOMAIN_SEPARATOR(), transferStructHash)
|
|
);
|
|
(uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPk, transferDigest);
|
|
|
|
vm.prank(spender);
|
|
vm.expectRevert(
|
|
abi.encodeWithSelector(
|
|
CompliantFiatTokenV2.AuthorizationAlreadyUsed.selector,
|
|
owner,
|
|
nonce
|
|
)
|
|
);
|
|
token.transferWithAuthorization(owner, recipient, value, validAfter, validBefore, nonce, v, r, s);
|
|
}
|
|
|
|
function testMintAndBurnReasonHashesEmitStructuredEvents() public {
|
|
bytes32 mintReason = keccak256("mint-reason");
|
|
bytes32 burnReason = keccak256("burn-reason");
|
|
uint256 mintAmount = 10_000 * 10 ** 6;
|
|
|
|
vm.expectEmit(true, true, true, false);
|
|
emit CompliantOperationDeclared(
|
|
token.OPERATION_MINT(),
|
|
admin,
|
|
admin,
|
|
admin,
|
|
address(0),
|
|
recipient,
|
|
mintAmount,
|
|
mintReason,
|
|
bytes32(0),
|
|
bytes32(0),
|
|
bytes32(0)
|
|
);
|
|
|
|
vm.prank(admin);
|
|
token.mint(recipient, mintAmount, mintReason);
|
|
|
|
assertEq(token.balanceOf(recipient), mintAmount);
|
|
|
|
vm.expectEmit(true, true, true, false);
|
|
emit CompliantOperationDeclared(
|
|
token.OPERATION_BURN(),
|
|
recipient,
|
|
recipient,
|
|
burner,
|
|
recipient,
|
|
address(0),
|
|
mintAmount / 2,
|
|
burnReason,
|
|
bytes32(0),
|
|
bytes32(0),
|
|
bytes32(0)
|
|
);
|
|
|
|
vm.prank(burner);
|
|
token.burn(recipient, mintAmount / 2, burnReason);
|
|
|
|
assertEq(token.balanceOf(recipient), mintAmount / 2);
|
|
}
|
|
|
|
function testPauseIsRoleGated() public {
|
|
vm.prank(spender);
|
|
vm.expectRevert();
|
|
token.pause();
|
|
|
|
vm.prank(admin);
|
|
token.pause();
|
|
assertTrue(token.paused());
|
|
}
|
|
|
|
function testSupplyControlsEnforceCap() public {
|
|
uint256 configuredCap = token.totalSupply() + (1_000 * 10 ** 6);
|
|
|
|
vm.prank(admin);
|
|
token.setSupplyControls(configuredCap, 0, 0);
|
|
|
|
vm.prank(admin);
|
|
token.mint(recipient, 1_000 * 10 ** 6, keccak256("under-cap"));
|
|
|
|
uint256 attemptedTotalSupply = token.totalSupply() + 1;
|
|
vm.prank(admin);
|
|
vm.expectRevert(
|
|
abi.encodeWithSelector(
|
|
CompliantFiatTokenV2.SupplyCapExceeded.selector,
|
|
configuredCap,
|
|
attemptedTotalSupply
|
|
)
|
|
);
|
|
token.mint(recipient, 1, keccak256("over-cap"));
|
|
}
|
|
|
|
function testLegacyAliasAndForwardCanonicalMetadata() public {
|
|
vm.prank(admin);
|
|
vm.expectRevert();
|
|
token.setForwardCanonical(false);
|
|
|
|
vm.startPrank(governanceExecutor);
|
|
token.addLegacyAlias("cEURC-legacy");
|
|
token.setForwardCanonical(false);
|
|
token.setSymbolDisplay("cEURC");
|
|
vm.stopPrank();
|
|
|
|
string[] memory aliases = token.legacyAliases();
|
|
assertEq(aliases.length, 1);
|
|
assertEq(aliases[0], "cEURC-legacy");
|
|
assertFalse(token.forwardCanonical());
|
|
assertEq(token.symbolDisplay(), "cEURC");
|
|
}
|
|
|
|
function testGovernanceAndSupervisionMetadataCanBeManaged() public {
|
|
bytes32 governanceProfileId = keccak256("gov-profile");
|
|
bytes32 supervisionProfileId = keccak256("supervision-profile");
|
|
bytes32 storageNamespace = keccak256("storage-namespace");
|
|
|
|
vm.prank(admin);
|
|
vm.expectRevert();
|
|
token.setGovernanceProfileId(governanceProfileId);
|
|
|
|
vm.startPrank(governanceExecutor);
|
|
token.setGovernanceProfileId(governanceProfileId);
|
|
token.setSupervisionProfileId(supervisionProfileId);
|
|
token.setStorageNamespace(storageNamespace);
|
|
token.setPrimaryJurisdiction("EU");
|
|
token.setRegulatoryDisclosureURI("ipfs://disclosure");
|
|
token.setReportingURI("ipfs://reporting");
|
|
token.setCanonicalUnderlyingAsset(address(0x1234));
|
|
token.setSupervisionConfiguration(true, true, 14 days);
|
|
vm.stopPrank();
|
|
|
|
assertEq(token.governanceProfileId(), governanceProfileId);
|
|
assertEq(token.supervisionProfileId(), supervisionProfileId);
|
|
assertEq(token.storageNamespace(), storageNamespace);
|
|
assertEq(token.primaryJurisdiction(), "EU");
|
|
assertEq(token.regulatoryDisclosureURI(), "ipfs://disclosure");
|
|
assertEq(token.reportingURI(), "ipfs://reporting");
|
|
assertEq(token.canonicalUnderlyingAsset(), address(0x1234));
|
|
assertTrue(token.supervisionRequired());
|
|
assertTrue(token.governmentApprovalRequired());
|
|
assertEq(token.minimumUpgradeNoticePeriod(), 14 days);
|
|
assertFalse(token.wrappedTransport());
|
|
}
|
|
|
|
function testEip5267DomainIntrospectionMatchesTokenMetadata() public view {
|
|
(
|
|
bytes1 fields,
|
|
string memory name,
|
|
string memory version,
|
|
uint256 chainId,
|
|
address verifyingContract,
|
|
bytes32 salt,
|
|
uint256[] memory extensions
|
|
) = token.eip712Domain();
|
|
|
|
assertEq(uint8(fields), uint8(0x0f));
|
|
assertEq(name, "Euro Coin (Compliant V2)");
|
|
assertEq(version, "2");
|
|
assertEq(chainId, block.chainid);
|
|
assertEq(verifyingContract, address(token));
|
|
assertEq(salt, bytes32(0));
|
|
assertEq(extensions.length, 0);
|
|
}
|
|
|
|
function testEmergencyMetadataOverridesRemainAvailableOutsideGovernance() public {
|
|
vm.prank(admin);
|
|
token.emergencySetPresentationMetadata(true, "ipfs://emergency-token", "cEURC-emergency");
|
|
|
|
vm.prank(admin);
|
|
token.emergencySetGovernanceMetadata(
|
|
keccak256("emergency-gov"),
|
|
keccak256("emergency-sup"),
|
|
keccak256("emergency-storage"),
|
|
"Emergency Jurisdiction",
|
|
address(0x9999),
|
|
true,
|
|
true,
|
|
30 days
|
|
);
|
|
|
|
vm.prank(admin);
|
|
token.emergencySetDisclosureMetadata("ipfs://emergency-disclosure", "ipfs://emergency-reporting");
|
|
|
|
assertTrue(token.forwardCanonical());
|
|
assertEq(token.tokenURI(), "ipfs://emergency-token");
|
|
assertEq(token.symbolDisplay(), "cEURC-emergency");
|
|
assertEq(token.primaryJurisdiction(), "Emergency Jurisdiction");
|
|
assertEq(token.canonicalUnderlyingAsset(), address(0x9999));
|
|
assertEq(token.regulatoryDisclosureURI(), "ipfs://emergency-disclosure");
|
|
assertEq(token.reportingURI(), "ipfs://emergency-reporting");
|
|
assertEq(token.minimumUpgradeNoticePeriod(), 30 days);
|
|
}
|
|
}
|