wormhole/ethereum/forge-test/TokenImplementation.t.sol

960 lines
30 KiB
Solidity

// SPDX-License-Identifier: Apache 2
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import "../contracts/bridge/token/TokenImplementation.sol";
import "forge-std/Test.sol";
import "forge-std/console.sol";
contract TestTokenImplementation is TokenImplementation, Test {
uint256 constant SECP256K1_CURVE_ORDER =
115792089237316195423570985008687907852837564279074904382605163141518161494337;
struct InitiateParameters {
string name;
string symbol;
uint8 decimals;
uint64 sequence;
address owner;
uint16 chainId;
bytes32 nativeContract;
}
struct SignatureSetup {
address allower;
bytes32 r;
bytes32 s;
uint8 v;
}
function setupTestEnvironmentWithInitialize() public {
InitiateParameters memory init;
init.name = "Valuable Token";
init.symbol = "VALU";
init.decimals = 8;
init.sequence = 1;
init.owner = _msgSender();
init.chainId = 5;
init
.nativeContract = 0x1337133713371337133713371337133713371337133713371337133713371337;
initialize(
init.name,
init.symbol,
init.decimals,
init.sequence,
init.owner,
init.chainId,
init.nativeContract
);
}
function setupTestEnvironmentWithOldInitialize() public {
InitiateParameters memory init;
init.name = "Old Valuable Token";
init.symbol = "OLD";
init.decimals = 8;
init.sequence = 1;
init.owner = _msgSender();
init.chainId = 5;
init
.nativeContract = 0x1337133713371337133713371337133713371337133713371337133713371337;
_initializeNativeToken(
init.name,
init.symbol,
init.decimals,
init.sequence,
init.owner,
init.chainId,
init.nativeContract
);
}
function simulatePermitSignature(
bytes32 walletPrivateKey,
address spender,
uint256 amount,
uint256 deadline
) public returns (SignatureSetup memory output) {
// prepare signer allowing for tokens to be spent
uint256 sk = uint256(walletPrivateKey);
output.allower = vm.addr(sk);
bytes32 PERMIT_TYPEHASH = keccak256(
"Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"
);
bytes32 structHash = keccak256(
abi.encode(
PERMIT_TYPEHASH,
output.allower,
spender,
amount,
nonces(output.allower),
deadline
)
);
bytes32 message = ECDSA.toTypedDataHash(DOMAIN_SEPARATOR(), structHash);
(output.v, output.r, output.s) = vm.sign(sk, message);
}
// if any of these tests fail, you may have messed around with the
// existing storage slots
function testCheckStorageSlots() public {
// initialize TokenImplementation
setupTestEnvironmentWithInitialize();
// mint some so we can check totalSupply and balances
uint256 mintedAmount = 42069;
_mint(_msgSender(), mintedAmount);
// also set allowances
uint256 allowanceAmount = 69420;
address spender = address(0x1);
_approve(_msgSender(), spender, allowanceAmount);
// slot 0: name (string)
{
bytes32 data = vm.load(address(this), bytes32(0));
// length 14, name = "Valuable Token"
// data <= 31 bytes long, so length is stored at end as length * 2
bytes memory expectedName = bytes("Valuable Token");
require(
uint256(data) & uint256(255) == expectedName.length * 2,
"incorrect name length"
);
require(
uint256(data) & uint256(255) == bytes(_state.name).length * 2,
"incorrect name length"
);
for (uint256 i = 0; i < expectedName.length; ++i) {
// I don't care to save this variable to storage
require(
data[i] == expectedName[i],
"data[i] != expectedName[i]"
);
require(
data[i] == bytes(_state.name)[i],
"data[i] != _state.name[i]"
);
}
}
// slot 1: symbol (string)
{
bytes32 data = vm.load(address(this), bytes32(uint256(1)));
// length 14, name = "Valuable Token"
// data <= 31 bytes long, so length is stored at end as length * 2
bytes memory expectedSymbol = bytes("VALU");
require(
uint256(data) & uint256(255) == expectedSymbol.length * 2,
"incorrect symbol length"
);
require(
uint256(data) & uint256(255) == bytes(_state.symbol).length * 2,
"incorrect symbol length"
);
for (uint256 i = 0; i < expectedSymbol.length; ++i) {
// I don't care to save this variable to storage
require(
data[i] == expectedSymbol[i],
"data[i] != expectedSymbol[i]"
);
require(
data[i] == bytes(_state.symbol)[i],
"data[i] != _state.symbol[i]"
);
}
}
// slot 2: metaLastUpdatedSequence (uint64)
{
bytes32 data = vm.load(address(this), bytes32(uint256(2)));
require(
uint256(data) == uint256(1),
"data != expected metaLastUpdatedSequence"
);
require(
uint256(data) == uint256(_state.metaLastUpdatedSequence),
"data != _state.metaLastUpdatedSequence"
);
}
// slot 3: totalSupply (uint256)
{
// now verify
bytes32 data = vm.load(address(this), bytes32(uint256(3)));
require(
uint256(data) == mintedAmount,
"data != expected totalSupply"
);
require(
uint256(data) == uint256(_state.totalSupply),
"data != _state.totalSupply"
);
}
// slot 4: decimals (uint8)
{
bytes32 data = vm.load(address(this), bytes32(uint256(4)));
require(uint256(data) == uint256(8), "data != expected decimals");
require(
uint256(data) == uint256(_state.decimals),
"data != _state.decimals"
);
}
// slot 5: balances (mapping(address) => uint256)
{
bytes32 data = vm.load(address(this), bytes32(uint256(5)));
require(uint256(data) == uint256(0), "data != 0");
bytes32 mappedData = vm.load(
address(this),
keccak256(abi.encode(_msgSender(), 5))
);
require(
uint256(mappedData) == mintedAmount,
"data != expected balance for account"
);
require(
uint256(mappedData) == _state.balances[_msgSender()],
"data != _state.balances[_msgSender()]"
);
}
// slot 6: allowances (mapping(address) => uint256)
{
bytes32 data = vm.load(address(this), bytes32(uint256(6)));
require(uint256(data) == uint256(0), "data != 0");
bytes32 mappedData = vm.load(
address(this),
keccak256(
abi.encode(spender, keccak256(abi.encode(_msgSender(), 6)))
)
);
require(
uint256(mappedData) == allowanceAmount,
"data != expected allowance for account"
);
require(
uint256(mappedData) == _state.allowances[_msgSender()][spender],
"data != _state.allowances[_msgSender()][spender]"
);
}
// slot 7: owner (address), initialized (bool), chainId (uint16)
{
bytes32 data = vm.load(address(this), bytes32(uint256(7)));
require(
(uint256(data) >> (21 * 8)) == uint256(5),
"data[9:11] != expected chainId"
);
require(
(uint256(data) >> (21 * 8)) == uint256(_state.chainId),
"data[9:11] != _state.chainId"
);
require(
uint8(data[11]) == uint8(1),
"data[11] != expected initialized"
);
require(
uint8(data[11]) == uint8(_state.initialized ? 1 : 0),
"data[11] != _state.initialized"
);
require(
uint256(data) & uint256(2**160 - 1) ==
uint256(uint160(_msgSender())),
"data[12:32] != expected owner"
);
require(
uint256(data) & uint256(2**160 - 1) ==
uint256(uint160(_state.owner)),
"data[12:32] != _state.owner"
);
}
// slot 8: nativeContract (bytes32)
{
bytes32 data = vm.load(address(this), bytes32(uint256(8)));
require(
data ==
0x1337133713371337133713371337133713371337133713371337133713371337,
"data != expected nativeContract"
);
require(
data == _state.nativeContract,
"data != _state.nativeContract"
);
}
// slot 9: cachedDomainSeparator (bytes32)
{
bytes32 data = vm.load(address(this), bytes32(uint256(9)));
require(
data ==
_buildDomainSeparator(
_eip712DomainNameHashed(),
_eip712DomainSalt()
),
"data != expected domain separator"
);
require(data == DOMAIN_SEPARATOR(), "data != DOMAIN_SEPARATOR()");
require(
data == _state.cachedDomainSeparator,
"data != _state.cachedDomainSeparator"
);
}
// slot 10: cachedChainId (uint256)
{
bytes32 data = vm.load(address(this), bytes32(uint256(10)));
require(uint256(data) == block.chainid, "data != block.chainid");
require(
uint256(data) == _state.cachedChainId,
"data != _state.cachedChainId"
);
}
// slot 11: cachedThis (address)
{
bytes32 data = vm.load(address(this), bytes32(uint256(11)));
require(
uint256(data) == uint256(uint160(address(this))),
"data != address(this)"
);
require(
uint256(data) == uint256(uint160(_state.cachedThis)),
"data != _state.cachedThis"
);
}
// slot 12: cachedSalt (bytes32)
{
bytes32 data = vm.load(address(this), bytes32(uint256(12)));
require(data == _eip712DomainSalt(), "data != expected salt");
require(data == _state.cachedSalt, "data != _state.cachedSalt");
}
// slot 13: cachedHashedName (bytes32)
{
bytes32 data = vm.load(address(this), bytes32(uint256(13)));
require(
data == _eip712DomainNameHashed(),
"data != _eip712DomainNameHashed()"
);
require(
data == _state.cachedHashedName,
"data != _state.cachedHashedName"
);
}
}
function testPermit(
bytes32 walletPrivateKey,
uint256 amount,
address spender
) public {
vm.assume(walletPrivateKey != bytes32(0));
vm.assume(uint256(walletPrivateKey) < SECP256K1_CURVE_ORDER);
vm.assume(spender != address(0));
// initialize TokenImplementation
setupTestEnvironmentWithInitialize();
// prepare signer allowing for tokens to be spent
uint256 deadline = 10;
SignatureSetup memory signature = simulatePermitSignature(
walletPrivateKey,
spender,
amount,
deadline
);
// set allowance with permit
permit(
signature.allower,
spender,
amount,
deadline,
signature.v,
signature.r,
signature.s
);
require(
allowance(signature.allower, spender) == amount,
"allowance incorrect"
);
}
function testFailPermitWithSameSignature(
bytes32 walletPrivateKey,
uint256 amount,
address spender
) public {
vm.assume(walletPrivateKey != bytes32(0));
vm.assume(uint256(walletPrivateKey) < SECP256K1_CURVE_ORDER);
vm.assume(spender != address(0));
// initialize TokenImplementation
setupTestEnvironmentWithInitialize();
// prepare signer allowing for tokens to be spent
uint256 deadline = 10;
SignatureSetup memory signature = simulatePermitSignature(
walletPrivateKey,
spender,
amount,
deadline
);
// set allowance with permit
permit(
signature.allower,
spender,
amount,
deadline,
signature.v,
signature.r,
signature.s
);
// try again... you shall not pass
// NOTE: using "testFail" instead of "test" because
// vm.expectRevert("ERC20Permit: invalid signature") does not work
permit(
signature.allower,
spender,
amount,
deadline,
signature.v,
signature.r,
signature.s
);
}
function testFailPermitWithBadSignature(
bytes32 walletPrivateKey,
uint256 amount,
address spender
) public {
vm.assume(walletPrivateKey != bytes32(0));
vm.assume(uint256(walletPrivateKey) < SECP256K1_CURVE_ORDER);
vm.assume(spender != address(0));
// initialize TokenImplementation
setupTestEnvironmentWithInitialize();
// avoid overflow for this test
uint256 wrongAmount;
unchecked {
wrongAmount = amount + 1; // amount will never equal
}
// prepare signer allowing for tokens to be spent
uint256 deadline = 10;
SignatureSetup memory signature = simulatePermitSignature(
walletPrivateKey,
spender,
wrongAmount,
deadline
);
// you shall not pass!
// NOTE: using "testFail" instead of "test" because
// vm.expectRevert("ERC20Permit: invalid signature") does not work
permit(
signature.allower,
spender,
amount,
deadline,
signature.v,
signature.r,
signature.s
);
}
function testPermitWithSignatureUsedAfterDeadline(
bytes32 walletPrivateKey,
uint256 amount,
address spender
) public {
vm.assume(walletPrivateKey != bytes32(0));
vm.assume(uint256(walletPrivateKey) < SECP256K1_CURVE_ORDER);
vm.assume(spender != address(0));
// initialize TokenImplementation
setupTestEnvironmentWithInitialize();
// prepare signer allowing for tokens to be spent
uint256 deadline = 10;
SignatureSetup memory signature = simulatePermitSignature(
walletPrivateKey,
spender,
amount,
deadline
);
// waited too long
vm.warp(deadline + 1);
// and fail
vm.expectRevert("ERC20Permit: expired deadline");
permit(
signature.allower,
spender,
amount,
deadline,
signature.v,
signature.r,
signature.s
);
}
function testInitializePermitState() public {
// initialize TokenImplementation as if it were the old implementation
setupTestEnvironmentWithOldInitialize();
require(
_state.cachedHashedName == bytes32(0),
"cachedHashedName is set"
);
require(_state.cachedSalt == bytes32(0), "cachedSalt is set");
// explicity call private method
_initializePermitStateIfNeeded();
require(
_state.cachedHashedName == _eip712DomainNameHashed(),
"hasnedName not cached"
);
require(_state.cachedSalt == _eip712DomainSalt(), "salt not cached");
// check permit state variables
require(
_state.cachedChainId == block.chainid,
"_state.cachedChainId != expected"
);
require(
_state.cachedDomainSeparator ==
keccak256(
abi.encode(
keccak256(
"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract,bytes32 salt)"
),
keccak256(abi.encodePacked(name())),
keccak256(abi.encodePacked(_eip712DomainVersion())),
block.chainid,
address(this),
keccak256(abi.encodePacked(chainId(), nativeContract()))
)
),
"_state.cachedDomainSeparator != expected"
);
require(
_buildDomainSeparator(
_eip712DomainNameHashed(),
_eip712DomainSalt()
) ==
keccak256(
abi.encode(
keccak256(
"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract,bytes32 salt)"
),
keccak256(abi.encodePacked(name())),
keccak256(abi.encodePacked(_eip712DomainVersion())),
block.chainid,
address(this),
keccak256(abi.encodePacked(chainId(), nativeContract()))
)
),
"_buildDomainSeparator() != expected"
);
require(
_state.cachedThis == address(this),
"_state.cachedThis != expected"
);
}
function testPermitForPreviouslyDeployedImplementation(
bytes32 walletPrivateKey,
uint256 amount,
address spender
) public {
vm.assume(walletPrivateKey != bytes32(0));
vm.assume(uint256(walletPrivateKey) < SECP256K1_CURVE_ORDER);
vm.assume(spender != address(0));
// initialize TokenImplementation as if it were the old implementation
setupTestEnvironmentWithOldInitialize();
require(_state.cachedSalt == bytes32(0), "cachedSalt is set");
// prepare signer allowing for tokens to be spent
uint256 deadline = 10;
SignatureSetup memory signature = simulatePermitSignature(
walletPrivateKey,
spender,
amount,
deadline
);
// set allowance with permit
permit(
signature.allower,
spender,
amount,
deadline,
signature.v,
signature.r,
signature.s
);
require(
allowance(signature.allower, spender) == amount,
"allowance incorrect"
);
}
// used to prevent stack too deep in test
struct Eip712DomainOutput {
bytes1 fields;
string name;
string version;
uint256 chainId;
address verifyingContract;
bytes32 salt;
uint256[] extensions;
}
function testPermitUsingEip712DomainValues(
bytes32 walletPrivateKey,
uint256 amount,
address spender
) public {
vm.assume(walletPrivateKey != bytes32(0));
vm.assume(uint256(walletPrivateKey) < SECP256K1_CURVE_ORDER);
vm.assume(spender != address(0));
// initialize TokenImplementation
setupTestEnvironmentWithInitialize();
Eip712DomainOutput memory domain;
(
domain.fields,
domain.name,
domain.version,
domain.chainId,
domain.verifyingContract,
domain.salt,
domain.extensions
) = eip712Domain();
require(domain.fields == hex"1F", "domainFields != expected");
require(
keccak256(abi.encodePacked(domain.name)) ==
keccak256(abi.encodePacked(name())),
"domainName != expected"
);
require(
keccak256(abi.encodePacked(domain.name)) ==
_eip712DomainNameHashed(),
"domainName != _eip712DomainNameHashed()"
);
require(
keccak256(abi.encodePacked(domain.version)) ==
keccak256(abi.encodePacked("1")),
"domainVersion != expected"
);
require(
keccak256(abi.encodePacked(domain.version)) ==
keccak256(abi.encodePacked(_eip712DomainVersion())),
"domainVersion != _eip712DomainVersion()"
);
require(domain.chainId == block.chainid, "domainFields != expected");
require(
domain.chainId == _state.cachedChainId,
"domainFields != _state.cachedChainId"
);
require(
domain.verifyingContract == address(this),
"domainVerifyingContract != expected"
);
require(
domain.verifyingContract == _state.cachedThis,
"domainVerifyingContract != _state.cachedThis"
);
require(
domain.salt ==
keccak256(abi.encodePacked(chainId(), nativeContract())),
"domainFields != expected"
);
require(
domain.salt == _eip712DomainSalt(),
"domainFields != _eip712DomainSalt()"
);
require(
domain.salt == _state.cachedSalt,
"domainFields != _state.cachedSalt"
);
require(domain.extensions.length == 0, "domainExtensions.length != 0");
// prepare signer allowing for tokens to be spent
SignatureSetup memory signature;
uint256 sk = uint256(walletPrivateKey);
signature.allower = vm.addr(sk);
uint256 deadline = 10;
bytes32 structHash = keccak256(
abi.encode(
keccak256(
"Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"
),
signature.allower,
spender,
amount,
nonces(signature.allower),
deadline
)
);
// build domain separator by hand using eip712Domain() output
bytes32 domainSeparator = keccak256(
abi.encode(
keccak256(
"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract,bytes32 salt)"
),
keccak256(abi.encodePacked(domain.name)),
keccak256(abi.encodePacked(domain.version)),
domain.chainId,
domain.verifyingContract,
domain.salt
)
);
// sign and set allowance with permit
(signature.v, signature.r, signature.s) = vm.sign(
sk,
ECDSA.toTypedDataHash(domainSeparator, structHash)
);
permit(
signature.allower,
spender,
amount,
deadline,
signature.v,
signature.r,
signature.s
);
require(
allowance(signature.allower, spender) == amount,
"allowance incorrect"
);
}
function testPermitAfterUpdateDetails(
bytes32 walletPrivateKey,
uint256 amount,
address spender,
string calldata newName
) public {
vm.assume(walletPrivateKey != bytes32(0));
vm.assume(uint256(walletPrivateKey) < SECP256K1_CURVE_ORDER);
vm.assume(spender != address(0));
vm.assume(bytes(newName).length <= 32);
// initialize TokenImplementation
setupTestEnvironmentWithInitialize();
string memory oldName = name();
bytes32 oldDomainSeparator = _state.cachedDomainSeparator;
// permit before updateDetails
{
uint256 deadline = 10;
SignatureSetup memory signature = simulatePermitSignature(
walletPrivateKey,
spender,
amount,
deadline
);
// set allowance with permit
permit(
signature.allower,
spender,
amount,
deadline,
signature.v,
signature.r,
signature.s
);
require(
allowance(signature.allower, spender) == amount,
"allowance incorrect"
);
// revoke allowance to prep for next test
_approve(signature.allower, spender, 0);
}
// asset metadata updated here
updateDetails(
newName,
"NEW", // new symbol
_state.metaLastUpdatedSequence + 1 // new sequence
);
require(
keccak256(abi.encodePacked(newName)) !=
keccak256(abi.encodePacked(oldName)),
"newName == oldName"
);
require(
_domainSeparatorV4() != oldDomainSeparator,
"_domainSeparatorV4() == oldDomainSeparator"
);
require(
_state.cachedDomainSeparator != oldDomainSeparator,
"_state.cachedDomainSeparator == oldDomainSeparator"
);
require(
_state.cachedDomainSeparator == _domainSeparatorV4(),
"_state.cachedDomainSeparator != _domainSeparatorV4()"
);
// permit after updateDetails
{
uint256 deadline = 10;
SignatureSetup memory signature = simulatePermitSignature(
walletPrivateKey,
spender,
amount,
deadline
);
// set allowance with permit
permit(
signature.allower,
spender,
amount,
deadline,
signature.v,
signature.r,
signature.s
);
require(
allowance(signature.allower, spender) == amount,
"allowance incorrect"
);
}
}
function testPermitForOldSalt(
bytes32 walletPrivateKey,
uint256 amount,
address spender
) public {
vm.assume(walletPrivateKey != bytes32(0));
vm.assume(uint256(walletPrivateKey) < SECP256K1_CURVE_ORDER);
vm.assume(spender != address(0));
// initialize TokenImplementation
setupTestEnvironmentWithInitialize();
// hijack salt
_state.cachedSalt = keccak256(abi.encodePacked("definitely not right"));
require(
_state.cachedSalt != _eip712DomainSalt(),
"_state.cachedSalt == _eip712DomainSalt()"
);
// prepare signer allowing for tokens to be spent
uint256 deadline = 10;
SignatureSetup memory signature = simulatePermitSignature(
walletPrivateKey,
spender,
amount,
deadline
);
// set allowance with permit
permit(
signature.allower,
spender,
amount,
deadline,
signature.v,
signature.r,
signature.s
);
// verify salt is correct
require(
_state.cachedSalt == _eip712DomainSalt(),
"_state.cachedSalt != _eip712DomainSalt()"
);
// then allowance
require(
allowance(signature.allower, spender) == amount,
"allowance incorrect"
);
}
function testPermitForOldName(
bytes32 walletPrivateKey,
uint256 amount,
address spender
) public {
vm.assume(walletPrivateKey != bytes32(0));
vm.assume(uint256(walletPrivateKey) < SECP256K1_CURVE_ORDER);
vm.assume(spender != address(0));
// initialize TokenImplementation
setupTestEnvironmentWithInitialize();
// hijack name
_state.cachedHashedName = keccak256("definitely not right");
require(
_state.cachedHashedName != _eip712DomainNameHashed(),
"_state.cachedHashedName == _eip712DomainNameHashed()"
);
// prepare signer allowing for tokens to be spent
uint256 deadline = 10;
SignatureSetup memory signature = simulatePermitSignature(
walletPrivateKey,
spender,
amount,
deadline
);
// set allowance with permit
permit(
signature.allower,
spender,
amount,
deadline,
signature.v,
signature.r,
signature.s
);
// verify name is correct
require(
_state.cachedHashedName == _eip712DomainNameHashed(),
"_state.cachedHashedName != _eip712DomainNameHashed()"
);
// then allowance
require(
allowance(signature.allower, spender) == amount,
"allowance incorrect"
);
}
}