User based testing pattern
A common pattern used in dapptools and foundry projects is a 'User Contract'. It is an abstraction over the contract interaction that looks something like this (from DSToken)
contract TokenUser {
DSToken token;
constructor(DSToken token_) public {
token = token_;
}
function doApprove(address spender, uint amount)
public
returns (bool)
{
return token.approve(spender, amount);
}
//...
}
And in the test contract, we can use it like this
user1 = new TokenUser(token);
user1.doApprove(user2);
This is an interesting pattern as it lets you wrap contracts and test them like how a user would interact with them. Its advantage shines really well when you need to test with multiple user addresses.
Blacksmiths to use the foundry
I took this pattern a step further and created a full-fledged contract generator that will create these 'User Contracts', along with a bunch of UX niceties. You can create a user with a particular address or private key and it can perform all operations an EOA can. All this can be automated by running the blacksmith.js script (sorry, not in rust yet). It automatically creates User contracts for all contracts in your foundry project directory.
Features
Wrap multiple target contracts
user1.dex.swap(100);
user1.factory.pause(true);
user1.token.transfer(user2.addr, 100);
Sign using private key
(uint8 v, bytes32 r, bytes32 s) = user1.sign("blacksmith");
Call arbiraty contracts
user1.call{value:10}(contract_address, "calldata");
Set user address's balance
user1.deal(100);
Zero code size at address
user1.addr.code.length // is zero
The blacksmith.js script creates the baseBlacksmith.sol
that contains basic functions like call
, sign
and deal
. It also creates TargetBS.sol
for all Target
contracts in the project directory. To Base User contract (Blacksmith) takes in an address and a private key as constructor params. If the private key is zero, the provided address is used as the user's address, else the address is calculated from the private key.
constructor( address _addr, uint256 _privateKey, address _target) {
addr = _privateKey == 0 ? _addr : bsvm.addr(_privateKey);
privateKey = _privateKey;
target = payable(_target);
}
To create a User contract to interact with a Target
contract, you import TargetBS
contract. Along with the address and private key, it also takes in the target contract's address
constructor( address _addr, uint256 _privateKey, address _target) {
addr = _privateKey == 0 ? _addr : bsvm.addr(_privateKey);
privateKey = _privateKey;
target = payable(_target);
}
To create a user object, you can create a struct and add the required interface. The below code is all you'll need to write to get started with testing. Rest is taken care of by blacksmith script.
struct User {
address addr; // to avoid external call, we save it in the struct
Blacksmith base; // contains call(), sign(), deal()
FooTokenBS foo; // interacts with FooToken contract
BarTokenBS bar; // interacts with BarToken contract
}
function createUser(address _addr, uint256 _privateKey) public returns (User memory) {
Blacksmith base = new Blacksmith(_addr, _privateKey);
FooTokenBS _foo = new FooTokenBS(_addr, _privateKey, address(foo));
BarTokenBS _bar = new BarTokenBS(_addr, _privateKey, address(bar));
base.deal(100);
return User(base.addr(), base, _foo, _bar);
}
function setUp() public {
foo = new FooToken();
bar = new BarToken();
alice = createUser(address(0), 111); // addrss will be 0x052b91ad9732d1bce0ddae15a4545e5c65d02443
bob = createUser(address(111), 0); // address will be 0x000000000000000000000000000000000000006f
eve = createUser(address(0), 0); // address will be 0x0000000000000000000000000000000000000000
}
Now you can use it directly in your test functions
function testSomething() public {
bob.foo.approve(alice.addr, 10);
alice.foo.transferFrom(bob.addr, alice.addr, 10);
alice.bar.approve(eve.addr, 100);
eve.transferFrom(bob.addr, eve.addr, 50);
eve.call{value:10}(alice.ddr, "");
(uint8 v, bytes32 r, bytes32 s) = alice.sign("blacksmith");
}
Usage
To get started with blacksmith, download blacksmith.js to the foundry projectβs root directory.
curl -O https://raw.githubusercontent.com/pbshgthm/blacksmith/main/blacksmith.js
node blacksmith.js create #in foundry project's root directory
This will run forge build
and then create /src/test/blacksmith
directory with user contracts in Target.bs.sol
.