Initial commit

This commit is contained in:
Jayden Windle
2022-11-29 14:17:49 -08:00
parent 14d6f4f993
commit 9a9d9c8e72
6 changed files with 753 additions and 38 deletions

4
remappings.txt Normal file
View File

@@ -0,0 +1,4 @@
ds-test/=lib/forge-std/lib/ds-test/src/
erc4626-tests/=lib/openzeppelin-contracts/lib/erc4626-tests/
forge-std/=lib/forge-std/src/
openzeppelin-contracts/=lib/openzeppelin-contracts/contracts/

View File

@@ -1,14 +0,0 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
contract Counter {
uint256 public number;
function setNumber(uint256 newNumber) public {
number = newNumber;
}
function increment() public {
number++;
}
}

119
src/Vault.sol Normal file
View File

@@ -0,0 +1,119 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "forge-std/Script.sol";
import "openzeppelin-contracts/proxy/Clones.sol";
import "openzeppelin-contracts/proxy/utils/Initializable.sol";
import "openzeppelin-contracts/token/ERC721/IERC721.sol";
import "openzeppelin-contracts/token/ERC721/IERC721Receiver.sol";
import "openzeppelin-contracts/token/ERC1155/IERC1155Receiver.sol";
import "openzeppelin-contracts/utils/cryptography/ECDSA.sol";
import "./VaultRegistry.sol";
error AlreadyInitialized();
contract Vault is Initializable {
// before any transfer
// check nft ownership
// extensible as fuck
VaultRegistry vaultRegistry;
function initialize(address _vaultRegistry) public initializer {
vaultRegistry = VaultRegistry(_vaultRegistry);
}
modifier onlyOwner(address tokenCollection, uint256 tokenId) {
require(
msg.sender == IERC721(tokenCollection).ownerOf(tokenId),
"Not owner"
);
_;
}
modifier onlyVault(address tokenCollection, uint256 tokenId) {
require(
address(this) ==
address(vaultRegistry.getVault(tokenCollection, tokenId)),
"Not vault"
);
_;
}
function execTransaction(
address payable to,
uint256 value,
bytes calldata data,
address tokenCollection,
uint256 tokenId
)
public
payable
onlyVault(tokenCollection, tokenId)
onlyOwner(tokenCollection, tokenId)
returns (bool success, bytes memory returnData)
{
(success, returnData) = to.call{value: value}(data);
}
function isValidSignature(
bytes32 _hash,
bytes memory _signature,
address tokenCollection,
uint256 tokenId
)
public
view
onlyVault(tokenCollection, tokenId)
returns (bytes4 magicValue)
{
(address signer, ECDSA.RecoverError error) = ECDSA.tryRecover(
_hash,
_signature
);
if (
error == ECDSA.RecoverError.NoError &&
signer == IERC721(tokenCollection).ownerOf(tokenId)
) {
return this.isValidSignature.selector;
}
}
// receiver functions
receive() external payable {}
fallback() external payable {}
function onERC721Received(
address,
address,
uint256,
bytes calldata
) external pure returns (bytes4) {
return IERC721Receiver.onERC721Received.selector;
}
function onERC1155Received(
address,
address,
uint256,
uint256,
bytes calldata /* data */
) external pure returns (bytes4) {
return IERC1155Receiver.onERC1155Received.selector;
}
function onERC1155BatchReceived(
address,
address,
uint256[] calldata,
uint256[] calldata,
bytes calldata
) external pure returns (bytes4) {
return IERC1155Receiver.onERC1155BatchReceived.selector;
}
}

61
src/VaultRegistry.sol Normal file
View File

@@ -0,0 +1,61 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "forge-std/Script.sol";
import "openzeppelin-contracts/proxy/Clones.sol";
import "openzeppelin-contracts/proxy/beacon/BeaconProxy.sol";
import "openzeppelin-contracts/proxy/utils/Initializable.sol";
import "openzeppelin-contracts/token/ERC721/IERC721.sol";
import "openzeppelin-contracts/utils/Create2.sol";
import "./Vault.sol";
contract VaultRegistry {
address public vaultBeacon;
constructor(address _vaultBeacon) {
vaultBeacon = _vaultBeacon;
}
function getVault(address tokenCollection, uint256 tokenId)
public
view
returns (address payable)
{
bytes32 salt = keccak256(abi.encodePacked(tokenCollection, tokenId));
bytes memory creationCode = type(BeaconProxy).creationCode;
bytes memory initializerCall = abi.encodeWithSignature(
"initialize(address)",
address(this)
);
bytes32 bytecodeHash = keccak256(
abi.encodePacked(
creationCode,
abi.encode(vaultBeacon, initializerCall)
)
);
address predictedVaultAddress = Create2.computeAddress(
salt,
bytecodeHash
);
return payable(predictedVaultAddress);
}
function deployVault(address tokenCollection, uint256 tokenId)
public
returns (address payable)
{
bytes32 salt = keccak256(abi.encodePacked(tokenCollection, tokenId));
bytes memory initializerCall = abi.encodeWithSignature(
"initialize(address)",
address(this)
);
address vaultAddress = address(
new BeaconProxy{salt: salt}(vaultBeacon, initializerCall)
);
return payable(vaultAddress);
}
}

View File

@@ -1,24 +0,0 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "forge-std/Test.sol";
import "../src/Counter.sol";
contract CounterTest is Test {
Counter public counter;
function setUp() public {
counter = new Counter();
counter.setNumber(0);
}
function testIncrement() public {
counter.increment();
assertEq(counter.number(), 1);
}
function testSetNumber(uint256 x) public {
counter.setNumber(x);
assertEq(counter.number(), x);
}
}

569
test/Vault.t.sol Normal file
View File

@@ -0,0 +1,569 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "forge-std/Test.sol";
import "openzeppelin-contracts/proxy/Clones.sol";
import "openzeppelin-contracts/token/ERC721/ERC721.sol";
import "openzeppelin-contracts/token/ERC1155/ERC1155.sol";
import "openzeppelin-contracts/token/ERC20/ERC20.sol";
import "openzeppelin-contracts/proxy/beacon/UpgradeableBeacon.sol";
import "../src/Vault.sol";
import "../src/VaultRegistry.sol";
contract VaultCollectionTest is Test {
DummyERC721 public dummyERC721;
DummyERC1155 public dummyERC1155;
DummyERC20 public dummyERC20;
Vault public vaultImplementation;
UpgradeableBeacon public vaultBeacon;
VaultRegistry public vaultRegistry;
TokenCollection public tokenCollection;
event Initialized(uint8 version);
function setUp() public {
dummyERC721 = new DummyERC721();
dummyERC1155 = new DummyERC1155();
dummyERC20 = new DummyERC20();
vaultImplementation = new Vault();
vaultBeacon = new UpgradeableBeacon(address(vaultImplementation));
vaultRegistry = new VaultRegistry(address(vaultBeacon));
tokenCollection = new TokenCollection();
}
function testDeployVault() public {
assertTrue(address(vaultRegistry) != address(0));
uint16 tokenId = 1;
address predictedVaultAddress = vaultRegistry.getVault(
address(tokenCollection),
tokenId
);
// expect vault to be initialized on creation
vm.expectEmit(
true,
false,
false,
false,
address(predictedVaultAddress)
);
emit Initialized(1);
address vaultAddress = vaultRegistry.deployVault(
address(tokenCollection),
tokenId
);
assertTrue(vaultAddress != address(0));
assertTrue(vaultAddress == predictedVaultAddress);
}
function testTransferETHPreDeploy() public {
address user1 = vm.addr(1);
vm.deal(user1, 0.2 ether);
uint256 tokenId = 1;
// get address that vault will be deployed to (before token is minted)
address payable vaultAddress = vaultRegistry.getVault(
address(tokenCollection),
tokenId
);
// mint token for vault to user1
tokenCollection.mint(user1, tokenId);
assertEq(tokenCollection.ownerOf(tokenId), user1);
// send ETH from user1 to vault (prior to vault deployment)
vm.prank(user1);
(bool sent, ) = vaultAddress.call{value: 0.2 ether}("");
assertTrue(sent);
assertEq(vaultAddress.balance, 0.2 ether);
// deploy vault contract (from a different wallet)
address payable createdVaultInstance = vaultRegistry.deployVault(
address(tokenCollection),
tokenId
);
assertEq(vaultAddress, createdVaultInstance);
Vault vault = Vault(vaultAddress);
// user1 executes transaction to send ETH from vault
vm.prank(user1);
vault.execTransaction(
payable(user1),
0.1 ether,
"",
address(tokenCollection),
tokenId
);
// success!
assertEq(vaultAddress.balance, 0.1 ether);
assertEq(user1.balance, 0.1 ether);
}
function testTransferETHPostDeploy() public {
address user1 = vm.addr(1);
vm.deal(user1, 0.2 ether);
uint256 tokenId = 2;
address payable vaultAddress = vaultRegistry.deployVault(
address(tokenCollection),
tokenId
);
tokenCollection.mint(user1, tokenId);
assertEq(tokenCollection.ownerOf(tokenId), user1);
vm.prank(user1);
(bool sent, ) = vaultAddress.call{value: 0.2 ether}("");
assertTrue(sent);
assertEq(vaultAddress.balance, 0.2 ether);
Vault vault = Vault(vaultAddress);
vm.prank(user1);
vault.execTransaction(
payable(user1),
0.1 ether,
"",
address(tokenCollection),
tokenId
);
assertEq(vaultAddress.balance, 0.1 ether);
assertEq(user1.balance, 0.1 ether);
}
function testTransferERC20PreDeploy() public {
address user1 = vm.addr(1);
uint256 tokenId = 3;
address payable computedVaultInstance = vaultRegistry.getVault(
address(tokenCollection),
tokenId
);
tokenCollection.mint(user1, tokenId);
assertEq(tokenCollection.ownerOf(tokenId), user1);
dummyERC20.mint(computedVaultInstance, 1 ether);
assertEq(dummyERC20.balanceOf(computedVaultInstance), 1 ether);
address payable vaultAddress = vaultRegistry.deployVault(
address(tokenCollection),
tokenId
);
Vault vault = Vault(vaultAddress);
bytes memory erc20TransferCall = abi.encodeWithSignature(
"transfer(address,uint256)",
user1,
1 ether
);
vm.prank(user1);
vault.execTransaction(
payable(address(dummyERC20)),
0,
erc20TransferCall,
address(tokenCollection),
tokenId
);
assertEq(dummyERC20.balanceOf(vaultAddress), 0);
assertEq(dummyERC20.balanceOf(user1), 1 ether);
}
function testTransferERC20PostDeploy() public {
address user1 = vm.addr(1);
uint256 tokenId = 4;
address payable vaultAddress = vaultRegistry.deployVault(
address(tokenCollection),
tokenId
);
tokenCollection.mint(user1, tokenId);
assertEq(tokenCollection.ownerOf(tokenId), user1);
dummyERC20.mint(vaultAddress, 1 ether);
assertEq(dummyERC20.balanceOf(vaultAddress), 1 ether);
Vault vault = Vault(vaultAddress);
bytes memory erc20TransferCall = abi.encodeWithSignature(
"transfer(address,uint256)",
user1,
1 ether
);
vm.prank(user1);
vault.execTransaction(
payable(address(dummyERC20)),
0,
erc20TransferCall,
address(tokenCollection),
tokenId
);
assertEq(dummyERC20.balanceOf(vaultAddress), 0);
assertEq(dummyERC20.balanceOf(user1), 1 ether);
}
function testTransferERC1155PreDeploy() public {
address user1 = vm.addr(1);
uint256 tokenId = 5;
address payable computedVaultInstance = vaultRegistry.getVault(
address(tokenCollection),
tokenId
);
tokenCollection.mint(user1, tokenId);
assertEq(tokenCollection.ownerOf(tokenId), user1);
dummyERC1155.mint(computedVaultInstance, 1, 10);
assertEq(dummyERC1155.balanceOf(computedVaultInstance, 1), 10);
address payable vaultAddress = vaultRegistry.deployVault(
address(tokenCollection),
tokenId
);
Vault vault = Vault(vaultAddress);
bytes memory erc1155TransferCall = abi.encodeWithSignature(
"safeTransferFrom(address,address,uint256,uint256,bytes)",
vaultAddress,
user1,
1,
10,
""
);
vm.prank(user1);
vault.execTransaction(
payable(address(dummyERC1155)),
0,
erc1155TransferCall,
address(tokenCollection),
tokenId
);
assertEq(dummyERC1155.balanceOf(vaultAddress, 1), 0);
assertEq(dummyERC1155.balanceOf(user1, 1), 10);
}
function testTransferERC1155PostDeploy() public {
address user1 = vm.addr(1);
uint256 tokenId = 6;
address payable vaultAddress = vaultRegistry.deployVault(
address(tokenCollection),
tokenId
);
tokenCollection.mint(user1, tokenId);
assertEq(tokenCollection.ownerOf(tokenId), user1);
dummyERC1155.mint(vaultAddress, 1, 10);
assertEq(dummyERC1155.balanceOf(vaultAddress, 1), 10);
Vault vault = Vault(vaultAddress);
bytes memory erc1155TransferCall = abi.encodeWithSignature(
"safeTransferFrom(address,address,uint256,uint256,bytes)",
vaultAddress,
user1,
1,
10,
""
);
vm.prank(user1);
vault.execTransaction(
payable(address(dummyERC1155)),
0,
erc1155TransferCall,
address(tokenCollection),
tokenId
);
assertEq(dummyERC1155.balanceOf(vaultAddress, 1), 0);
assertEq(dummyERC1155.balanceOf(user1, 1), 10);
}
function testTransferERC721PreDeploy() public {
address user1 = vm.addr(1);
uint256 tokenId = 7;
address payable computedVaultInstance = vaultRegistry.getVault(
address(tokenCollection),
tokenId
);
tokenCollection.mint(user1, tokenId);
assertEq(tokenCollection.ownerOf(tokenId), user1);
dummyERC721.mint(computedVaultInstance, 1);
assertEq(dummyERC721.balanceOf(computedVaultInstance), 1);
assertEq(dummyERC721.ownerOf(1), computedVaultInstance);
address payable vaultAddress = vaultRegistry.deployVault(
address(tokenCollection),
tokenId
);
Vault vault = Vault(vaultAddress);
bytes memory erc721TransferCall = abi.encodeWithSignature(
"safeTransferFrom(address,address,uint256)",
address(vaultAddress),
user1,
1
);
vm.prank(user1);
vault.execTransaction(
payable(address(dummyERC721)),
0,
erc721TransferCall,
address(tokenCollection),
tokenId
);
assertEq(dummyERC721.balanceOf(address(vaultAddress)), 0);
assertEq(dummyERC721.balanceOf(user1), 1);
assertEq(dummyERC721.ownerOf(1), user1);
}
function testTransferERC721PostDeploy() public {
address user1 = vm.addr(1);
uint256 tokenId = 8;
address payable vaultAddress = vaultRegistry.deployVault(
address(tokenCollection),
tokenId
);
tokenCollection.mint(user1, tokenId);
assertEq(tokenCollection.ownerOf(tokenId), user1);
dummyERC721.mint(vaultAddress, 1);
assertEq(dummyERC721.balanceOf(vaultAddress), 1);
assertEq(dummyERC721.ownerOf(1), vaultAddress);
Vault vault = Vault(vaultAddress);
bytes memory erc721TransferCall = abi.encodeWithSignature(
"safeTransferFrom(address,address,uint256)",
vaultAddress,
user1,
1
);
vm.prank(user1);
vault.execTransaction(
payable(address(dummyERC721)),
0,
erc721TransferCall,
address(tokenCollection),
tokenId
);
assertEq(dummyERC721.balanceOf(vaultAddress), 0);
assertEq(dummyERC721.balanceOf(user1), 1);
assertEq(dummyERC721.ownerOf(1), user1);
}
function testNonOwnerCallsFail() public {
address user1 = vm.addr(1);
address user2 = vm.addr(2);
uint256 tokenId = 9;
tokenCollection.mint(user1, tokenId);
assertEq(tokenCollection.ownerOf(tokenId), user1);
address payable vaultAddress = vaultRegistry.deployVault(
address(tokenCollection),
tokenId
);
vm.deal(vaultAddress, 1 ether);
Vault vault = Vault(vaultAddress);
// should fail if user2 tries to use vault
vm.prank(user2);
vm.expectRevert(bytes("Not owner"));
(bool success, ) = vault.execTransaction(
payable(user2),
0.1 ether,
"",
address(tokenCollection),
tokenId
);
assertEq(success, false);
}
function testVaultOwnershipTransfer() public {
address user1 = vm.addr(1);
address user2 = vm.addr(2);
uint256 tokenId = 10;
tokenCollection.mint(user1, tokenId);
assertEq(tokenCollection.ownerOf(tokenId), user1);
address payable vaultAddress = vaultRegistry.deployVault(
address(tokenCollection),
tokenId
);
vm.deal(vaultAddress, 1 ether);
Vault vault = Vault(vaultAddress);
// should fail if user2 tries to use vault
vm.prank(user2);
vm.expectRevert(bytes("Not owner"));
(bool success1, ) = vault.execTransaction(
payable(user2),
0.1 ether,
"",
address(tokenCollection),
tokenId
);
assertEq(success1, false);
vm.prank(user1);
tokenCollection.safeTransferFrom(user1, user2, tokenId);
// should succeed now that user2 is owner
vm.prank(user2);
(bool success2, ) = vault.execTransaction(
payable(user2),
0.1 ether,
"",
address(tokenCollection),
tokenId
);
assertEq(success2, true);
assertEq(user2.balance, 0.1 ether);
}
function testMessageSigningAndVerificationForAuthorizedUser() public {
address user1 = vm.addr(1);
uint256 tokenId = 11;
tokenCollection.mint(user1, tokenId);
assertEq(tokenCollection.ownerOf(tokenId), user1);
address payable vaultAddress = vaultRegistry.deployVault(
address(tokenCollection),
tokenId
);
Vault vault = Vault(vaultAddress);
bytes32 hash = keccak256("This is a signed message");
(uint8 v1, bytes32 r1, bytes32 s1) = vm.sign(1, hash);
bytes memory signature1 = abi.encodePacked(r1, s1, v1);
bytes4 returnValue1 = vault.isValidSignature(
hash,
signature1,
address(tokenCollection),
tokenId
);
assertEq(returnValue1, vault.isValidSignature.selector);
}
function testMessageSigningAndVerificationForUnauthorizedUser() public {
address user1 = vm.addr(1);
uint256 tokenId = 11;
tokenCollection.mint(user1, tokenId);
assertEq(tokenCollection.ownerOf(tokenId), user1);
address payable vaultAddress = vaultRegistry.deployVault(
address(tokenCollection),
tokenId
);
Vault vault = Vault(vaultAddress);
bytes32 hash = keccak256("This is a signed message");
(uint8 v2, bytes32 r2, bytes32 s2) = vm.sign(2, hash);
bytes memory signature2 = abi.encodePacked(r2, s2, v2);
bytes4 returnValue2 = vault.isValidSignature(
hash,
signature2,
address(tokenCollection),
tokenId
);
assertEq(returnValue2, 0);
}
}
contract TokenCollection is ERC721 {
constructor() ERC721("TokenCollection", "TC") {}
function mint(address to, uint256 tokenId) external {
_safeMint(to, tokenId);
}
}
contract DummyERC721 is ERC721 {
constructor() ERC721("DummyERC721", "T721") {}
function mint(address to, uint256 tokenId) external {
_safeMint(to, tokenId);
}
}
contract DummyERC1155 is ERC1155 {
constructor() ERC1155("http://DummyERC1155.com") {}
function mint(
address to,
uint256 tokenId,
uint256 amount
) external {
_mint(to, tokenId, amount, "");
}
}
contract DummyERC20 is ERC20 {
constructor() ERC20("DummyERC20", "T20") {}
function mint(address to, uint256 amount) external {
_mint(to, amount);
}
}