Foundry is a fast testing and deployment tool for developing EVM smart contracts.
Tests are written in Solidity to keep the workflow consistent with smart contract development and testing before deployments.
Development contract for testing src/Contract.sol is called SimpleStorage for this example:
// SPDX-License-Identifier: MITpragma solidity 0.8.7;error sameStorageValue();error notOwner();error msgValueZero();contract SimpleStorage { uint public storedData; //Do not set 0 manually it wastes gas! uint public ownerUnixTimeContract; address public immutable owner; constructor() { owner = msg.sender; } event setOpenDataEvent(address indexed user, uint newValue); //Topics and other event arguments used for Foundry testing. Event arguments like this use gas in production so be careful. event setOwnerDataEvent(uint newOwnerUnixTime); event donateToOwnerEvent(); function set(uint x) public { if(storedData == x) { revert sameStorageValue(); } storedData = x; emit setOpenDataEvent(msg.sender, x); //Topic 1 (user) and other argument not indexed (newValue) for Foundry. } function setOwnerData() public { if(msg.sender != owner) { revert notOwner(); } ownerUnixTimeContract = block.timestamp; emit setOwnerDataEvent(block.timestamp); } function donateToOwner() public payable { if(msg.value == 0) { revert msgValueZero(); } payable(owner).transfer(address(this).balance); emit donateToOwnerEvent(); }}
Here we have a test contract which will test SimpleStorage located in test/Contract.t.sol called TestContract:
// SPDX-License-Identifier: Unlicensepragma solidity 0.8.7;import "forge-std/Test.sol";import "src/Contract.sol";contract TestContract is Test { //Functions fallback and receive used when the test contract is sent msg.value to prevent the test from reverting. fallback() external payable {} // Fallback function is called when msg.data is not empty receive() external payable {} // Function to receive Ether. msg.data must be empty //Define events here from other contracts since Foundry has trouble importing events from other contracts still. event setOpenDataEvent(address indexed user, uint newValue); event setOwnerDataEvent(uint newOwnerUnixTime); event donateToOwnerEvent(); SimpleStorage simpleStorageInstance; function setUp() public { simpleStorageInstance = new SimpleStorage(); } function testInitialStorage() public { assertEq(simpleStorageInstance.storedData(),0); assertEq(simpleStorageInstance.ownerUnixTimeContract(),0); assertEq(simpleStorageInstance.owner(),address(this)); } function testSetValidPath() public { assertEq(simpleStorageInstance.storedData(),0); vm.expectEmit(true,false,false,true); // Events have bool flags for indexed topic parameters in order (3 topics possible) along with arguments that might not be indexed (last flag). You can also check which address sent the event. emit setOpenDataEvent(address(this),1); simpleStorageInstance.set(1); assertEq(simpleStorageInstance.storedData(),1); } function testSetRevertPath() public { assertEq(simpleStorageInstance.storedData(),0); vm.expectRevert(sameStorageValue.selector); //Revert if the new value matches the current storage value. Custom error from SimpleStorage. simpleStorageInstance.set(0); assertEq(simpleStorageInstance.storedData(),0); } function testSetOwnerDataValidPath() public { assertEq(address(this),simpleStorageInstance.owner()); assertEq(simpleStorageInstance.ownerUnixTimeContract(),0); vm.expectEmit(false,false,false,true); // Events have bool flags for indexed topic parameters in order (3 topics possible) along with arguments that might not be indexed (last flag). You can also check which address sent the event. emit setOwnerDataEvent(10); vm.warp(10); //Increase block.timestamp by 10 seconds. simpleStorageInstance.setOwnerData(); assertEq(simpleStorageInstance.ownerUnixTimeContract(),10); } function testSetOwnerDataRevertPath() public { vm.startPrank(address(0)); //Change the address to not be the owner. The owner is address(this) in this context. assertEq(simpleStorageInstance.ownerUnixTimeContract(),0); vm.expectRevert(notOwner.selector); //Revert if not the owner. Custom error from SimpleStorage. simpleStorageInstance.setOwnerData(); } function testDonateToOwnerValidPath() public { uint ownerBalanceStart = address(this).balance; assertEq(ownerBalanceStart,79228162514264337593543950335); vm.deal(address(0),ownerBalanceStart); uint prankBalanceStart = address(this).balance; assertEq(ownerBalanceStart,79228162514264337593543950335); assertEq(address(simpleStorageInstance).balance, 0); vm.startPrank(address(0)); //Change the address to not be the owner. The owner is address(this) in this context. uint msgValueWei = 1; vm.expectEmit(false,false,false,false); // Events have bool flags for indexed topic parameters in order (3 topics possible) along with arguments that might not be indexed (last flag). You can also check which address sent the event. emit donateToOwnerEvent(); assertEq(address(simpleStorageInstance).balance, 0); simpleStorageInstance.donateToOwner{value: msgValueWei}(); vm.stopPrank(); //Stop prank since we don't need to be another address anymore for increasing the owner balance from a transfer. assertEq(address(simpleStorageInstance).balance, 0); assertEq(address(this).balance, ownerBalanceStart+1); assertEq(address(0).balance, prankBalanceStart-1); } function testDonateToOwnerRevertPath() public { vm.expectRevert(msgValueZero.selector); //Revert if MSG.VALUE is 0. Custom error from SimpleStorage. simpleStorageInstance.donateToOwner(); //MSG.VALUE is not set for call, so it is 0. }}
The test coverage above uses a local network to save time.
However, some contract applications integrate with contracts already deployed to a network.
If you want to test contracts deployed to specific networks and know your code coverage, run the following command with a RPC URL endpoint
for the --fork-url flag:
Local Unit Testing
Do local unit testing first before forking a network to speed up testing time.
If the following Goerli deployment worked, the following Shardeum deployment should work as well.
Legacy flag
The
--legacy
flag is used when deploying a smart contract using Legacy gas parameters for the transaction.
This is important, since Shardeum currently doesn't support EIP-1559.
Otherwise, you will see the following error when attempting to deploy to Shardeum:
EIP-1559 not activated
Deployment Info
The terminal might not notify you if the contract deployment worked.
Check the Shardeum Explorer after a transaction cycle to see if the contract deployed from the signer address from the private key used.