Protocol Hooks are Magna's offering to extend our escrow smart contract functionality at the protocol-level. This involves implementing our post-claim hook interface, which is a single function.
Need help designing or building your claim flow? Reach out to the Magna team for help!
Example Use Cases:
Claim and Stake
Stake tokens immediately upon claiming to secure your protocol and prevent sell pressure.
ClaimandBridge
Bridge tokens to a target chain automatically upon claiming.
Claim and Burn/Transfer
Re-route token amounts (e.g. royalities) depending on your customizable smart contract state.
Claim and Mint
Mint an NFT immediately upon claiming tokens to incentivize token engagement.
Implementation
Upon linking a hook contract to the Magna escrow contract (configurable in the Magna platform), claimed tokens will be sent to the hook contract to handle instead of directly to beneficiaries.
Note: it is extremely important that the hook contract (and all dependencies) are thoroughly audited. Magna does not provide any security guarantees on external contracts and may even reject your implementation if it does not uphold best practices.
Hook Contract Interface
Protocol Hook contracts should implement this following interface. All arguments must be defined in the function interface (even if they go unused).
pragma solidity ^0.8.24;
import {IERC20} from "@openzeppelin/interfaces/IERC20.sol";
/// @dev the address for denoting whether direct claims are allowed
IPostClaimHandler constant DIRECT_CLAIM_HANDLER = IPostClaimHandler(address(0));
/// @notice Interface for post claim handlers
interface IPostClaimHandler {
/**
* @notice Implementing this method provides a way to claim vesting tokens and execute some custom action afterwards
* @dev Implementors can assume that 'amount' amount of 'claimToken' has already been transferred to this contract address.
* Implementors should:
* 1. check if the calling contract is the vesting contract, and revert otherwise
* 2. revert the transaction, if for any reasons this contract cannot execute the custom actions
* @param claimToken Address of the vesting token.
* @param amount The amount of vesting tokens that were claimed and transferred to this contract address.
* @param originalBeneficiary The address of the user who was the original owner of the vesting tokens at the time the vesting contract was created.
* @param withdrawalAddress The latest owner of the vesting tokens which might be the same as the 'originalBeneficiary' in case no ownership transfer took place.
* @param allocationId The allocation id from which the withdrawn amount was taken.
* @param extraData Any abi encoded extra data that is necessary for the custom action. For example in case of a custom staking action, the user could state his
* staking preference by providing extraData.
*/
function handlePostClaim(
IERC20 claimToken,
uint256 amount,
address originalBeneficiary,
address withdrawalAddress,
string memory allocationId,
bytes memory extraData
) external;
}
Note that extraDatathat is passed from the Magna app is customizable. Please reach out to the Magna team for more deatils on how to configure this.
Example Implementation (Claim and Burn)
The below implementation implements a hook that burns tokens upon claiming with an optional donationRecipientstored in contract state.
pragma solidity =0.8.24;
import {IPostClaimHandler} from "../../src/interfaces/IPostClaimHandler.sol";
import {IERC20} from "@openzeppelin/interfaces/IERC20.sol";
import {SafeERC20} from "@openzeppelin/token/ERC20/utils/SafeERC20.sol";
import {Context} from "@openzeppelin/utils/Context.sol";
error InvalidCaller();
contract ClaimAndBurnHandler is IPostClaimHandler, Context {
using SafeERC20 for IERC20;
// some token implementation reject sending tokens to 0x00.00 address, so using 0xcc..cc here which is just as much unrecoverable as 0x00.00
address private constant BURN_ADDRESS = 0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC;
address public immutable vesterContract;
IERC20 public immutable burnToken;
address donationRecipient;
constructor(address vesterContract_, IERC20 burnToken_, address donationRecipient_) {
vesterContract = vesterContract_;
burnToken = burnToken_;
donationRecipient = donationRecipient_;
}
function handlePostClaim(
IERC20 claimToken,
uint256 amount,
address originalBeneficiary,
address withdrawalAddress,
string memory allocationId,
bytes memory extraData
) external {
if (_msgSender() != vesterContract) {
revert InvalidCaller();
}
bool sendDonation = abi.decode(extraData, (bool));
// 1. burn token is transferred from withdrawalAddress instead of originalBeneficiary for safety reasons
// 2. instead of calling the burn function which might not be implemented for all IERC20 tokens, tokens are sent to an unrecoverable BURN_ADDRESS address
burnToken.safeTransferFrom(withdrawalAddress, BURN_ADDRESS, amount);
if (sendDonation) {
uint256 donationAmount = amount / 10;
claimToken.safeTransfer(donationRecipient, donationAmount);
claimToken.safeTransfer(withdrawalAddress, amount - donationAmount);
} else {
claimToken.safeTransfer(withdrawalAddress, amount);
}
}
}