diff --git a/contracts/src/examples/modules/ECDSAModule.sol b/contracts/src/examples/modules/ECDSAModule.sol new file mode 100644 index 00000000..06847709 --- /dev/null +++ b/contracts/src/examples/modules/ECDSAModule.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.21; + +import { ECDSA } from "openzeppelin-contracts/contracts/utils/cryptography/ECDSA.sol"; +import { Ownable } from "openzeppelin-contracts/contracts/access/Ownable.sol"; + +import { AbstractModule } from "../../interface/AbstractModule.sol"; +import { AttestationPayload } from "../../types/Structs.sol"; + +/** + * @title ECDSA Module + * @author Consensys x DappSheriff + * @notice This contract illustrates a valid Module that is used to verify ECDSA signatures of payload + */ +contract ECDSAModule is AbstractModule, Ownable { + address public signer; + mapping(uint256 => bool) public usedNonces; // nonce => used + + constructor(address _signer) { + signer = _signer; + } + + /// @dev This empty method prevents Foundry from counting this contract in code coverage + function test() public {} + + /** + * @notice This method is used to run the module's validation logic + * @param attestationPayload - AttestationPayload containing the user address as `subject` and nonce as `attestationData` + * @param validationPayload - Payload encoded with abi.encode(uint256).toEthSignedMessageHash().sign(signer) + */ + function run( + AttestationPayload memory attestationPayload, + bytes memory validationPayload, + address /*txSender*/, + uint256 /*value*/ + ) public override { + address signee = abi.decode(attestationPayload.subject, (address)); + uint256 nonce = abi.decode(attestationPayload.attestationData, (uint256)); + if (usedNonces[nonce]) { + revert("Nonce already used"); + } + + bytes32 hash = ECDSA.toEthSignedMessageHash(abi.encodePacked(signee, nonce)); + require(ECDSA.recover(hash, validationPayload) == signer, "Wrong signature"); + + usedNonces[nonce] = true; + } + + function setSigner(address _signer) external onlyOwner { + signer = _signer; + } +} diff --git a/contracts/test/example/ECDSAModule.t.sol b/contracts/test/example/ECDSAModule.t.sol new file mode 100644 index 00000000..2bc3f736 --- /dev/null +++ b/contracts/test/example/ECDSAModule.t.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.21; + +import { Test } from "forge-std/Test.sol"; +import { ECDSA } from "openzeppelin-contracts/contracts/utils/cryptography/ECDSA.sol"; + +import { ECDSAModule } from "../../src/examples/modules/ECDSAModule.sol"; +import { AttestationPayload } from "../../src/types/Structs.sol"; + +contract ECDSAModuleTest is Test { + ECDSAModule private ecdsaModule; + address private signer; + uint256 private signerPk; + AttestationPayload private attestationPayload; + + address private user = makeAddr("veraxUser"); + uint256 private nonce = 1234567; + + function setUp() public { + (signer, signerPk) = makeAddrAndKey("veraxSigner"); + ecdsaModule = new ECDSAModule(signer); + + attestationPayload = AttestationPayload(bytes32(uint256(1234)), 0, abi.encode(user), abi.encode(nonce)); + } + + function test_ECDSAModule_validSignature() public { + bytes memory signature = makeSignature(signerPk); + + ecdsaModule.run(attestationPayload, signature, signer, 0); + } + + function test_ECDSAModule_revertInvalidSignature() public { + Account memory fakeSigner = makeAccount("user"); + + bytes memory signature = makeSignature(fakeSigner.key); + + vm.expectRevert("Wrong signature"); + ecdsaModule.run(attestationPayload, signature, user, 0); + } + + function test_ECDSAModule_revertNonceUsed() public { + bytes memory signature = makeSignature(signerPk); + + ecdsaModule.run(attestationPayload, signature, user, 0); + vm.expectRevert("Nonce already used"); + ecdsaModule.run(attestationPayload, signature, user, 0); + } + + function makeSignature(uint256 _signer) private view returns (bytes memory) { + bytes32 hash = ECDSA.toEthSignedMessageHash(abi.encodePacked(user, nonce)); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(_signer, hash); + return abi.encodePacked(r, s, v); + } +}