-
Notifications
You must be signed in to change notification settings - Fork 75
fix: separate refund claim when there is no token account #633
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Signed-off-by: Reinis Martinsons <reinis@umaproject.org>
Signed-off-by: Reinis Martinsons <reinis@umaproject.org>
Signed-off-by: Reinis Martinsons <reinis@umaproject.org>
Signed-off-by: Reinis Martinsons <reinis@umaproject.org>
| pub amount: u64, | ||
| } | ||
|
|
||
| // When executing relayer refund leaf, refund accounts are passed as remaining accounts and can hold either a regular |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
does this mean that, in theory, a relayer could close their ATA right before the bundle execution tx lands on-chain, thereby making the execution of the bundle in-valid as the remaining account type is wrong? so when the executor looks at the tx to send they build TokenAccount, expecting that the relayer has an ATA but before the tx lands on-chain the relayer can front run the tx and delete their ATA, which then requires, for this method to work, that the executor passes in the ClaimAccount, not token account, which breaks the bundle execution and the whole thing reverts?
In theory, this means that a relayer who can front run the execution can always "toggle" the status of their ATA (opening it or closing it), thereby blocking the bundle execution?
if the executor sends these execution transactions via private mempool they might be able to protect themselves (you cant close/open the account before the execution) but this is not going to be failsafe?
thoughts on this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In theory that's possible, but might be a bigger effort as there is no public mempool, so the attacker has to run Solana validator co-located close to our bot. We can reduce attack surface by sending the transaction to trusted validator. And if this is still an issue, we can fall-back using claim accounts after couple of reverts.
Signed-off-by: Reinis Martinsons <reinis@umaproject.org>
Signed-off-by: Reinis Martinsons <reinis@umaproject.org>
Signed-off-by: Reinis Martinsons <reinis@umaproject.org>
Signed-off-by: Reinis Martinsons <reinis@umaproject.org>
Signed-off-by: Reinis Martinsons <reinis@umaproject.org>
Signed-off-by: Reinis Martinsons <reinis@umaproject.org>
Signed-off-by: Reinis Martinsons <reinis@umaproject.org>
Signed-off-by: Reinis Martinsons <reinis@umaproject.org>
Signed-off-by: Reinis Martinsons <reinis@umaproject.org>
| where | ||
| 'c: 'info, | ||
| { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can you help me understand this bit of syntax a bit better? I also see it in the lib.rs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added the constraint that lifetime for remaining accounts ('c) is at least as long as 'info for accounts that are referenced by this account array. I think its needed because of manual remaining account processing in try_from_remaining_account method. Without these lifetime constraints the compiler errors.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
o that's interesting. ye, I've had other lifetime issues while implementing other things and was able to address it through other syntax but this is more complex here.
| @@ -0,0 +1,132 @@ | |||
| use anchor_lang::prelude::*; | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit on this file naming: we call the instruction refund_claims and the instruction refund_accounts. we should think about it makes sense to re-name this state to claim_account? equally, for other files between instruction & state having a consistent name between the two.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
RefundAccount can hold either TokenAccount or ClaimAccount and state/refund_account.rs has methods for handling both, so would not want to name it as claim_account. Also the wrapped RefundAccount is used in bundle instruction while individual ClaimAccount is used in refund_claims instruction, so there is no 1-1 relationship between state and instructions like we have in other places.
| // #[account( | ||
| // mut, | ||
| // seeds = [b"claim_account", mint.key().as_ref(), token_account.key().as_ref()], | ||
| // bump | ||
| // )] | ||
| // pub claim_account: Account<'info, ClaimAccount>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is this meant to be commented out?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, it is meant to document what Anchor macros this method is reimplementing, so its easier to audit knowing the intention of the implementation.
| describe("Execute Max Refunds", () => { | ||
| const executeMaxRefunds = async (refundType: RefundType) => { | ||
| const relayerRefundLeaves: RelayerRefundLeafType[] = []; | ||
| // Higher refund count hits inner instruction size limit when doing `emit_cpi` on public devnet. On localnet this is |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
to test this on testnet did you use the script?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it would be wise I think for us to run this same test on mainnet to ensure that there are no other memory issues that we don't know about on this. does mean deploying the whole program, but that's ok as we can always close it afterwards.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, here is the refund execution for 28 relayers. At 29 count would get this error: Instruction passed to inner instruction is too large (1306 > 1280) when doing emit_cpi.
Though, we might need to improve the script as in some runs it closed the instruction parameter account before leaf execution tx got mined.
| }); | ||
| }; | ||
|
|
||
| it("Execute Max Refunds to Token Accounts", async () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is cool. nice structure within the describe. these tests are now...complex, but that's ok.
| // #[account( | ||
| // mut, | ||
| // address = expected_token_account @ CustomError::InvalidRefund, | ||
| // token::mint = expected_mint, | ||
| // token::token_program = token_program | ||
| // )] | ||
| // pub token_account: InterfaceAccount<'info, TokenAccount>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is this mean to be commented out?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes, left them for auditing purposes as we reimplement what Anchor macro would have done on static account.
| // Emit the DeferredRelayerRefunds event if any claim accounts were passed instead of token accounts. We cannot emit | ||
| // individual accounts and amounts as that would run out of memory for larger leaves. | ||
| if deferred_refunds { | ||
| emit_cpi!(DeferredRelayerRefunds { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's ok to emit all this but it's not really needed, right? we really just want to know that it happened, but care less about all the other props give we have ExecutedRelayerRefundRoot.
an alternative structure could simply be to add a bool to ExecutedRelayerRefundRoot that indicates if deferred_refunds is set so we don't need a. new event?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes, if we can change interface of ExecutedRelayerRefundRoot this would be much easier to avoid separate emit_cpi
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
pushed the commit to include the bool in ExecutedRelayerRefundRoot
| } | ||
|
|
||
| // Checks if the token account is associated with the expected mint. | ||
| if &token_account.mint != expected_mint { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is a meta-comment but this check has a & (borrowing) and the others don't. expected_token_account is passed in & but is not checked against the account_info with borrowing.
point is: is there away to do this more consistently in this file and beyond?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not sure. owner and key in AccountInfo are borrowed references, while InterfaceAccount::try_from returns token_account with all of its properties owned. That's why we need to handle them differently. And since compiler automatically dereferences I think its more common pattern not to do explicit dereferencing, instead pass borrowed variables to comparison and let the compiler dereference them where needed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
but you're right that its not fully consistent with comparison in try_claim_account_from_account_info below, so I just pushed commit to use account_info.key != &pda_address instead of account_info.key() != pda_address on line 121.
Signed-off-by: Reinis Martinsons <reinis@umaproject.org>
Signed-off-by: Reinis Martinsons <reinis@umaproject.org>
Signed-off-by: Reinis Martinsons <reinis@umaproject.org>
Signed-off-by: Reinis Martinsons <reinis@umaproject.org>
This fix protects against a relayer passing invalid refund token account to block the whole relayer leaf execution. If a relayer had provided invalid ATA for refunds, the leaf executor can replace it with a claim account PDA that is derived from mint and relayer's token account to only accrue given relayer's refund claims. If this was a valid token address, but was closed at the time of leaf execution, all accrued claims can be refunded separately whenever the given refund account is created.
For the sake of code simplicity, this does not attempt to initialize a claim account when executing the relayer refund leaf. The operator is expected to initialize it in advance, but this is required only when a given relayer's token account is invalid.